appmap 0.43.0 → 0.44.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -67,7 +67,7 @@ module AppMap
67
67
 
68
68
  response = JSON.generate \
69
69
  version: AppMap::APPMAP_FORMAT_VERSION,
70
- classMap: AppMap.class_map(tracer.event_methods, include_source: AppMap.include_source?),
70
+ classMap: AppMap.class_map(tracer.event_methods),
71
71
  metadata: metadata,
72
72
  events: @events
73
73
 
@@ -26,8 +26,9 @@ module AppMap
26
26
  end
27
27
 
28
28
 
29
- def finish
29
+ def finish(exception)
30
30
  warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
31
+ warn "Exception: #{exception}" if exception && AppMap::Minitest::LOG
31
32
 
32
33
  events = []
33
34
  AppMap.tracing.delete @trace
@@ -36,15 +37,17 @@ module AppMap
36
37
 
37
38
  AppMap::Minitest.add_event_methods @trace.event_methods
38
39
 
39
- class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
40
+ class_map = AppMap.class_map(@trace.event_methods)
40
41
 
41
42
  feature_group = test.class.name.underscore.split('_')[0...-1].join('_').capitalize
42
43
  feature_name = test.name.split('_')[1..-1].join(' ')
43
44
  scenario_name = [ feature_group, feature_name ].join(' ')
44
45
 
45
- AppMap::Minitest.save scenario_name,
46
- class_map,
47
- source_location,
46
+ AppMap::Minitest.save name: scenario_name,
47
+ class_map: class_map,
48
+ source_location: source_location,
49
+ test_status: exception ? 'failed' : 'succeeded',
50
+ exception: exception,
48
51
  events: events
49
52
  end
50
53
  end
@@ -63,11 +66,11 @@ module AppMap
63
66
  @recordings_by_test[test.object_id] = Recording.new(test, name)
64
67
  end
65
68
 
66
- def end_test(test)
69
+ def end_test(test, exception:)
67
70
  recording = @recordings_by_test.delete(test.object_id)
68
71
  return warn "No recording found for #{test}" unless recording
69
72
 
70
- recording.finish
73
+ recording.finish exception
71
74
  end
72
75
 
73
76
  def config
@@ -78,9 +81,9 @@ module AppMap
78
81
  @event_methods += event_methods
79
82
  end
80
83
 
81
- def save(example_name, class_map, source_location, events: nil, labels: nil)
84
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
82
85
  metadata = AppMap::Minitest.metadata.tap do |m|
83
- m[:name] = example_name
86
+ m[:name] = name
84
87
  m[:source_location] = source_location
85
88
  m[:app] = AppMap.configuration.name
86
89
  m[:frameworks] ||= []
@@ -91,6 +94,13 @@ module AppMap
91
94
  m[:recorder] = {
92
95
  name: 'minitest'
93
96
  }
97
+ m[:test_status] = test_status
98
+ if exception
99
+ m[:exception] = {
100
+ class: exception.class.name,
101
+ message: exception.to_s
102
+ }
103
+ end
94
104
  end
95
105
 
96
106
  appmap = {
@@ -99,14 +109,9 @@ module AppMap
99
109
  classMap: class_map,
100
110
  events: events
101
111
  }.compact
102
- fname = AppMap::Util.scenario_filename(example_name)
112
+ fname = AppMap::Util.scenario_filename(name)
103
113
 
104
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
105
- end
106
-
107
- def print_inventory
108
- class_map = AppMap.class_map(@event_methods)
109
- save 'Inventory', class_map, labels: %w[inventory]
114
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
110
115
  end
111
116
 
112
117
  def enabled?
@@ -115,9 +120,6 @@ module AppMap
115
120
 
116
121
  def run
117
122
  init
118
- at_exit do
119
- print_inventory
120
- end
121
123
  end
122
124
  end
123
125
  end
@@ -135,7 +137,7 @@ if AppMap::Minitest.enabled?
135
137
  begin
136
138
  run_without_hook
137
139
  ensure
138
- AppMap::Minitest.end_test self
140
+ AppMap::Minitest.end_test self, exception: $!
139
141
  end
140
142
  end
141
143
  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
@@ -94,8 +94,9 @@ module AppMap
94
94
  result
95
95
  end
96
96
 
97
- def finish
97
+ def finish(exception)
98
98
  warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
99
+ warn "Exception: #{exception}" if exception && AppMap::RSpec::LOG
99
100
 
100
101
  events = []
101
102
  AppMap.tracing.delete @trace
@@ -104,7 +105,7 @@ module AppMap
104
105
 
105
106
  AppMap::RSpec.add_event_methods @trace.event_methods
106
107
 
107
- class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
108
+ class_map = AppMap.class_map(@trace.event_methods)
108
109
 
109
110
  description = []
110
111
  scope = ScopeExample.new(example)
@@ -127,9 +128,11 @@ module AppMap
127
128
 
128
129
  full_description = normalize.call(description.join(' '))
129
130
 
130
- AppMap::RSpec.save full_description,
131
- class_map,
132
- 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,
133
136
  events: events
134
137
  end
135
138
  end
@@ -148,11 +151,11 @@ module AppMap
148
151
  @recordings_by_example[example.object_id] = Recording.new(example)
149
152
  end
150
153
 
151
- def end_spec(example)
154
+ def end_spec(example, exception:)
152
155
  recording = @recordings_by_example.delete(example.object_id)
153
156
  return warn "No recording found for #{example}" unless recording
154
157
 
155
- recording.finish
158
+ recording.finish exception
156
159
  end
157
160
 
158
161
  def config
@@ -163,12 +166,11 @@ module AppMap
163
166
  @event_methods += event_methods
164
167
  end
165
168
 
166
- def save(example_name, class_map, source_location, events: nil, labels: nil)
169
+ def save(name:, class_map:, source_location:, test_status:, exception:, events:)
167
170
  metadata = AppMap::RSpec.metadata.tap do |m|
168
- m[:name] = example_name
171
+ m[:name] = name
169
172
  m[:source_location] = source_location
170
173
  m[:app] = AppMap.configuration.name
171
- m[:labels] = labels if labels
172
174
  m[:frameworks] ||= []
173
175
  m[:frameworks] << {
174
176
  name: 'rspec',
@@ -177,6 +179,13 @@ module AppMap
177
179
  m[:recorder] = {
178
180
  name: 'rspec'
179
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
180
189
  end
181
190
 
182
191
  appmap = {
@@ -185,14 +194,9 @@ module AppMap
185
194
  classMap: class_map,
186
195
  events: events
187
196
  }.compact
188
- fname = AppMap::Util.scenario_filename(example_name)
189
-
190
- File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
191
- end
197
+ fname = AppMap::Util.scenario_filename(name)
192
198
 
193
- def print_inventory
194
- class_map = AppMap.class_map(@event_methods)
195
- save 'Inventory', class_map, labels: %w[inventory]
199
+ AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
196
200
  end
197
201
 
198
202
  def enabled?
@@ -201,9 +205,6 @@ module AppMap
201
205
 
202
206
  def run
203
207
  init
204
- at_exit do
205
- print_inventory
206
- end
207
208
  end
208
209
  end
209
210
  end
@@ -225,7 +226,7 @@ if AppMap::RSpec.enabled?
225
226
  begin
226
227
  instance_exec(&fn)
227
228
  ensure
228
- AppMap::RSpec.end_spec example
229
+ AppMap::RSpec.end_spec example, exception: $!
229
230
  end
230
231
  end
231
232
  end
data/lib/appmap/util.rb CHANGED
@@ -71,6 +71,22 @@ module AppMap
71
71
 
72
72
  event
73
73
  end
74
+
75
+ # Atomically writes AppMap data to +filename+.
76
+ def write_appmap(filename, appmap)
77
+ require 'fileutils'
78
+ require 'tmpdir'
79
+
80
+ # This is what Ruby Tempfile does; but we don't want the file to be unlinked.
81
+ mode = File::RDWR | File::CREAT | File::EXCL
82
+ ::Dir::Tmpname.create([ 'appmap_', '.json' ]) do |tmpname|
83
+ tempfile = File.open(tmpname, mode)
84
+ tempfile.write(appmap)
85
+ tempfile.close
86
+ # Atomically move the tempfile into place.
87
+ FileUtils.mv tempfile.path, filename
88
+ end
89
+ end
74
90
  end
75
91
  end
76
92
  end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.43.0'
6
+ VERSION = '0.44.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.4'
9
9
  end
data/patch ADDED
@@ -0,0 +1,1447 @@
1
+ diff --git a/.travis.yml b/.travis.yml
2
+ index ab6ccca..f165339 100644
3
+ --- a/.travis.yml
4
+ +++ b/.travis.yml
5
+ @@ -13,11 +13,26 @@ services:
6
+ # necessary.
7
+ before_script:
8
+ - unset RAILS_ENV
9
+ -
10
+ +
11
+ +cache:
12
+ + bundler: true
13
+ + directories:
14
+ + - $HOME/docker
15
+ +
16
+ +# https://stackoverflow.com/a/41975912
17
+ +before_cache:
18
+ + # Save tagged docker images
19
+ + - >
20
+ + mkdir -p $HOME/docker && docker images -a --filter='dangling=false' --format '{{.Repository}}:{{.Tag}} {{.ID}}'
21
+ + | xargs -n 2 -t sh -c 'test -e $HOME/docker/$1.tar.gz || docker save $0 | gzip -2 > $HOME/docker/$1.tar.gz'
22
+ +
23
+ +before_install:
24
+ + # Load cached docker images
25
+ + - if [[ -d $HOME/docker ]]; then ls $HOME/docker/*.tar.gz | xargs -I {file} sh -c "zcat {file} | docker load"; fi
26
+ +
27
+ jobs:
28
+ include:
29
+ - stage: test
30
+ script:
31
+ - mkdir tmp
32
+ - bundle exec rake test
33
+ -
34
+ diff --git a/CHANGELOG.md b/CHANGELOG.md
35
+ index e3ea210..4a25fba 100644
36
+ --- a/CHANGELOG.md
37
+ +++ b/CHANGELOG.md
38
+ @@ -1,3 +1,12 @@
39
+ +# v0.44.0
40
+ +
41
+ +* Support recording and labeling of indivudal functions via `functions:` section in *appmap.yml*.
42
+ +* Remove deprecated `exe/appmap`.
43
+ +* Add `test_status` and `exception` fields to AppMap metadata.
44
+ +* Write AppMap file atomically, by writing to a temp file first and then moving it into place.
45
+ +* Remove printing of `Inventory.json` file.
46
+ +* Remove source code from `classMap`.
47
+ +
48
+ # v0.43.0
49
+
50
+ * Record `name` and `class` of each entry in Hash-like parameters, messages, and return values.
51
+ diff --git a/README.md b/README.md
52
+ index b9e6eca..e5674d9 100644
53
+ --- a/README.md
54
+ +++ b/README.md
55
+ @@ -110,6 +110,9 @@ name: my_project
56
+ packages:
57
+ - path: app/controllers
58
+ - path: app/models
59
+ + # Exclude sub-paths within the package path
60
+ + exclude:
61
+ + - concerns/accessor
62
+ - path: app/jobs
63
+ - path: app/helpers
64
+ # Include the gems that you want to see in the dependency maps.
65
+ @@ -118,15 +121,22 @@ packages:
66
+ - gem: devise
67
+ - gem: aws-sdk
68
+ - gem: will_paginate
69
+ +# Global exclusion of a class name
70
+ exclude:
71
+ - MyClass
72
+ - MyClass#my_instance_method
73
+ - MyClass.my_class_method
74
+ +functions:
75
+ +- packages: myapp
76
+ + class: ControllerHelper
77
+ + function: logged_in_user
78
+ + labels: [ authentication ]
79
+ ```
80
+
81
+ * **name** Provides the project name (required)
82
+ * **packages** A list of source code directories which should be recorded.
83
+ * **exclude** A list of classes and/or methods to definitively exclude from recording.
84
+ +* **functions** A list of specific functions, scoped by package and class, to record.
85
+
86
+ **packages**
87
+
88
+ @@ -145,6 +155,11 @@ Each entry in the `packages` list is a YAML object which has the following keys:
89
+
90
+ Optional list of fully qualified class and method names. Separate class and method names with period (`.`) for class methods and hash (`#`) for instance methods.
91
+
92
+ +**functions**
93
+ +
94
+ +Optional list of `class, function` pairs. The `package` name is used to place the function within the class map, and does not have to match
95
+ +the folder or gem name. The primary use of `functions` is to apply specific labels to functions whose source code is not accessible (e.g., it's in a Gem).
96
+ +For functions which are part of the application code, use `@label` or `@labels` in code comments to apply labels.
97
+
98
+ # Labels
99
+
100
+ @@ -344,7 +359,7 @@ Each interactive diagram links directly to the source code, and the information
101
+ # AppMap Swagger
102
+
103
+ [appmap_swagger](https://github.com/applandinc/appmap_swagger-ruby) is a tool to generate Swagger files from AppMap data. With `appmap_swagger`, you can add Swagger to your Ruby or Ruby on Rails project, with no need to write or modify code. Use the Swagger UI to interact with your web services API as you build it, and use diffs of Swagger to perform code review of web service changes.
104
+ -
105
+ +n
106
+ # Uploading AppMaps
107
+
108
+ [https://app.land](https://app.land) can be used to store, analyze, and share AppMaps.
109
+ diff --git a/appmap.gemspec b/appmap.gemspec
110
+ index 7a01cb9..8b2af31 100644
111
+ --- a/appmap.gemspec
112
+ +++ b/appmap.gemspec
113
+ @@ -20,8 +20,6 @@ Gem::Specification.new do |spec|
114
+ ")
115
+ spec.extensions << "ext/appmap/extconf.rb"
116
+
117
+ - spec.bindir = 'exe'
118
+ - spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
119
+ spec.require_paths = ['lib']
120
+
121
+ spec.add_dependency 'activesupport'
122
+ diff --git a/exe/appmap b/exe/appmap
123
+ deleted file mode 100755
124
+ index c6bd228..0000000
125
+ --- a/exe/appmap
126
+ +++ /dev/null
127
+ @@ -1,154 +0,0 @@
128
+ -#!/usr/bin/env ruby
129
+ -# frozen_string_literal: true
130
+ -
131
+ -require 'gli'
132
+ -
133
+ -ENV['APPMAP_INITIALIZE'] = 'false'
134
+ -
135
+ -require 'appmap'
136
+ -require 'appmap/version'
137
+ -
138
+ -# AppMap CLI.
139
+ -module AppMap
140
+ - class App
141
+ - extend GLI::App
142
+ -
143
+ - program_desc 'AppMap client'
144
+ -
145
+ - version AppMap::VERSION
146
+ -
147
+ - subcommand_option_handling :normal
148
+ - arguments :strict
149
+ - preserve_argv true
150
+ -
151
+ - class << self
152
+ - protected
153
+ -
154
+ - def default_appmap_file
155
+ - ENV['APPMAP_FILE'] || 'appmap.json'
156
+ - end
157
+ -
158
+ - def output_file_flag(c, default_value: nil)
159
+ - c.desc 'Name of the output file'
160
+ - c.long_desc <<~DESC
161
+ - Use a single dash '-' for stdout.
162
+ - DESC
163
+ - c.default_value default_value if default_value
164
+ - c.arg_name 'filename'
165
+ - c.flag %i[o output]
166
+ - end
167
+ - end
168
+ -
169
+ - desc 'AppMap configuration file name'
170
+ - default_value ENV['APPMAP_CONFIG'] || 'appmap.yml'
171
+ - arg_name 'filename'
172
+ - flag %i[c config]
173
+ -
174
+ - desc 'Record the execution of a program and generate an AppMap.'
175
+ - arg_name 'program'
176
+ - command :record do |c|
177
+ - output_file_flag(c, default_value: default_appmap_file)
178
+ -
179
+ - c.action do |_, _, args|
180
+ - # My subcommand name
181
+ - ARGV.shift
182
+ -
183
+ - # Consume the :output option, if provided
184
+ - if %w[-o --output].find { |arg_name| ARGV[0] == arg_name.to_s }
185
+ - ARGV.shift
186
+ - ARGV.shift
187
+ - end
188
+ -
189
+ - # Name of the program to execute. GLI will ensure that it's present.
190
+ - program = args.shift or help_now!("'program' argument is required")
191
+ -
192
+ - # Also pop the program name from ARGV, because the command will use raw ARGV
193
+ - # to load the extra arguments into this Ruby process.
194
+ - ARGV.shift
195
+ -
196
+ - require 'appmap/command/record'
197
+ - AppMap::Command::Record.new(@config, program).perform do |version, metadata, class_map, events|
198
+ - @output_file.write JSON.generate(version: version,
199
+ - metadata: metadata,
200
+ - classMap: class_map,
201
+ - events: events)
202
+ - end
203
+ - end
204
+ - end
205
+ -
206
+ - desc 'Calculate and print statistics of scenario files.'
207
+ - arg_name 'filename'
208
+ - command :stats do |c|
209
+ - output_file_flag(c, default_value: '-')
210
+ -
211
+ - c.desc 'Display format for the result (text | json)'
212
+ - c.default_value 'text'
213
+ - c.flag %i[f format]
214
+ -
215
+ - c.desc 'Maximum number of lines to display for each stat'
216
+ - c.flag %i[l limit]
217
+ -
218
+ - c.action do |_, options, args|
219
+ - require 'appmap/command/stats'
220
+ -
221
+ - limit = options[:limit].to_i if options[:limit]
222
+ -
223
+ - # Name of the file to analyze. GLI will ensure that it's present.
224
+ - filenames = args
225
+ - help_now!("'filename' argument is required") if filenames.empty?
226
+ -
227
+ - require 'appmap/algorithm/stats'
228
+ - result = filenames.inject(::AppMap::Algorithm::Stats::Result.new([], [])) do |stats_result, filename|
229
+ - appmap = begin
230
+ - JSON.parse(File.read(filename))
231
+ - rescue JSON::ParserError
232
+ - STDERR.puts "#{filename} is not valid JSON : #{$!}"
233
+ - nil
234
+ - end
235
+ - stats_result.tap do
236
+ - if appmap
237
+ - limit = options[:limit].to_i if options[:limit]
238
+ - stats_for_file = AppMap::Command::Stats.new(appmap).perform(limit: limit)
239
+ - stats_result.merge!(stats_for_file)
240
+ - end
241
+ - end
242
+ - end
243
+ -
244
+ - result.sort!
245
+ - result.limit!(limit) if limit
246
+ -
247
+ - display = case options[:format]
248
+ - when 'json'
249
+ - JSON.pretty_generate(result.as_json)
250
+ - else
251
+ - result.as_text
252
+ - end
253
+ - @output_file.write display
254
+ - end
255
+ - end
256
+ -
257
+ - pre do |global, _, options, _|
258
+ - @config = interpret_config_option(global[:config])
259
+ - @output_file = interpret_output_file_option(options[:output])
260
+ -
261
+ - true
262
+ - end
263
+ -
264
+ - class << self
265
+ - protected
266
+ -
267
+ - def interpret_config_option(fname)
268
+ - AppMap.initialize fname
269
+ - end
270
+ -
271
+ - def interpret_output_file_option(file_name)
272
+ - Hash.new { |_, fname| -> { File.new(fname, 'w') } }.tap do |open_output_file|
273
+ - open_output_file[nil] = -> { nil }
274
+ - open_output_file['-'] = -> { $stdout }
275
+ - end[file_name].call
276
+ - end
277
+ - end
278
+ - end
279
+ -end
280
+ -
281
+ -exit AppMap::App.run(ARGV)
282
+ diff --git a/lib/appmap.rb b/lib/appmap.rb
283
+ index ccf7bb5..998513c 100644
284
+ --- a/lib/appmap.rb
285
+ +++ b/lib/appmap.rb
286
+ @@ -43,6 +43,7 @@ module AppMap
287
+ # Call this function before the program code is loaded by the Ruby VM, otherwise
288
+ # the load events won't be seen and the hooks won't activate.
289
+ def initialize(config_file_path = 'appmap.yml')
290
+ + raise "AppMap configuration file #{config_file_path} does not exist" unless ::File.exists?(config_file_path)
291
+ warn "Configuring AppMap from path #{config_file_path}"
292
+ Config.load_from_file(config_file_path).tap do |configuration|
293
+ self.configuration = configuration
294
+ @@ -50,11 +51,6 @@ module AppMap
295
+ end
296
+ end
297
+
298
+ - # Whether to include source and comments in all class maps.
299
+ - def include_source?
300
+ - ENV['APPMAP_SOURCE'] == 'true'
301
+ - end
302
+ -
303
+ # Used to start tracing, stop tracing, and record events.
304
+ def tracing
305
+ @tracing ||= Trace::Tracing.new
306
+ @@ -88,8 +84,8 @@ module AppMap
307
+ end
308
+
309
+ # Builds a class map from a config and a list of Ruby methods.
310
+ - def class_map(methods, options = {})
311
+ - ClassMap.build_from_methods(methods, options)
312
+ + def class_map(methods)
313
+ + ClassMap.build_from_methods(methods)
314
+ end
315
+
316
+ # Returns default metadata detected from the Ruby system and from the
317
+ diff --git a/lib/appmap/class_map.rb b/lib/appmap/class_map.rb
318
+ index 4d19a27..0023c1c 100644
319
+ --- a/lib/appmap/class_map.rb
320
+ +++ b/lib/appmap/class_map.rb
321
+ @@ -71,17 +71,17 @@ module AppMap
322
+ end
323
+
324
+ class << self
325
+ - def build_from_methods(methods, options = {})
326
+ + def build_from_methods(methods)
327
+ root = Types::Root.new
328
+ methods.each do |method|
329
+ - add_function root, method, options
330
+ + add_function root, method
331
+ end
332
+ root.children.map(&:to_h)
333
+ end
334
+
335
+ protected
336
+
337
+ - def add_function(root, method, include_source: true)
338
+ + def add_function(root, method)
339
+ package = method.package
340
+ static = method.static
341
+
342
+ @@ -113,16 +113,13 @@ module AppMap
343
+ [ method.defined_class, static ? '.' : '#', method.name ].join
344
+ end
345
+
346
+ - source, comment = begin
347
+ - [ method.source, method.comment ]
348
+ + comment = begin
349
+ + method.comment
350
+ rescue MethodSource::SourceNotFoundError
351
+ - [ nil, nil, ]
352
+ + nil
353
+ end
354
+
355
+ - if include_source
356
+ - function_info[:source] = source unless source.blank?
357
+ - function_info[:comment] = comment unless comment.blank?
358
+ - end
359
+ + function_info[:comment] = comment unless comment.blank?
360
+
361
+ function_info[:labels] = parse_labels(comment) + (package.labels || [])
362
+ object_infos << function_info
363
+ diff --git a/lib/appmap/command/record.rb b/lib/appmap/command/record.rb
364
+ index 5f22903..d683f63 100644
365
+ --- a/lib/appmap/command/record.rb
366
+ +++ b/lib/appmap/command/record.rb
367
+ @@ -27,7 +27,7 @@ module AppMap
368
+ event_thread.join
369
+ yield AppMap::APPMAP_FORMAT_VERSION,
370
+ AppMap.detect_metadata,
371
+ - AppMap.class_map(tracer.event_methods, include_source: AppMap.include_source?),
372
+ + AppMap.class_map(tracer.event_methods),
373
+ events
374
+ end
375
+
376
+ diff --git a/lib/appmap/config.rb b/lib/appmap/config.rb
377
+ index aa208d6..b4c8841 100644
378
+ --- a/lib/appmap/config.rb
379
+ +++ b/lib/appmap/config.rb
380
+ @@ -49,44 +49,89 @@ module AppMap
381
+ end
382
+ end
383
+
384
+ - Hook = Struct.new(:method_names, :package) do
385
+ + Function = Struct.new(:package, :cls, :labels, :function_names) do
386
+ + def to_h
387
+ + {
388
+ + package: package,
389
+ + class: cls,
390
+ + labels: labels,
391
+ + functions: function_names.map(&:to_sym)
392
+ + }.compact
393
+ + end
394
+ end
395
+
396
+ - OPENSSL_PACKAGES = Package.build_from_path('openssl', package_name: 'openssl', labels: %w[security crypto])
397
+ + class Hook
398
+ + attr_reader :method_names, :package
399
+ +
400
+ + def initialize(method_names, package)
401
+ + @method_names = method_names
402
+ + @package = package
403
+ + end
404
+ +
405
+ + def to_h
406
+ + {
407
+ + package: package.name,
408
+ + method_names: method_names
409
+ + }
410
+ + end
411
+ + end
412
+ +
413
+ + OPENSSL_PACKAGES = ->(labels) { Package.build_from_path('openssl', package_name: 'openssl', labels: labels) }
414
+
415
+ # Methods that should always be hooked, with their containing
416
+ # package and labels that should be applied to them.
417
+ HOOKED_METHODS = {
418
+ - 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', labels: %w[provider.secure_compare])),
419
+ + 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.build_from_path('active_support', labels: %w[crypto.secure_compare])),
420
+ 'ActionView::Renderer' => Hook.new(:render, Package.build_from_path('action_view', labels: %w[mvc.view])),
421
+ - 'ActionDispatch::Cookies::CookieJar' => Hook.new(%i[[]= clear update delete recycle], Package.build_from_path('action_pack', labels: %w[provider.http.cookie])),
422
+ - 'ActionDispatch::Cookies::EncryptedCookieJar' => Hook.new(%i[[]=], Package.build_from_path('action_pack', labels: %w[provider.http.cookie crypto])),
423
+ - 'CanCan::ControllerAdditions' => Hook.new(%i[authorize! can? cannot?], Package.build_from_path('cancancan', labels: %w[provider.authorization])),
424
+ - 'CanCan::Ability' => Hook.new(%i[authorize!], Package.build_from_path('cancancan', labels: %w[provider.authorization])),
425
+ + 'ActionDispatch::Request::Session' => Hook.new(%i[destroy [] dig values []= clear update delete fetch merge], Package.build_from_path('action_pack', labels: %w[http.session])),
426
+ + 'ActionDispatch::Cookies::CookieJar' => Hook.new(%i[[]= clear update delete recycle], Package.build_from_path('action_pack', labels: %w[http.cookie])),
427
+ + 'ActionDispatch::Cookies::EncryptedCookieJar' => Hook.new(%i[[]=], Package.build_from_path('action_pack', labels: %w[http.cookie crypto.encrypt])),
428
+ + 'CanCan::ControllerAdditions' => Hook.new(%i[authorize! can? cannot?], Package.build_from_path('cancancan', labels: %w[security.authorization])),
429
+ + 'CanCan::Ability' => Hook.new(%i[authorize!], Package.build_from_path('cancancan', labels: %w[security.authorization])),
430
+ + 'ActionController::Instrumentation' => [
431
+ + Hook.new(%i[process_action send_file send_data redirect_to], Package.build_from_path('action_view', labels: %w[mvc.controller])),
432
+ + Hook.new(%i[render], Package.build_from_path('action_view', labels: %w[mvc.view])),
433
+ + ]
434
+ }.freeze
435
+
436
+ BUILTIN_METHODS = {
437
+ - 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES),
438
+ - 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES),
439
+ - 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES),
440
+ - 'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGES),
441
+ - 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES),
442
+ - 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http io])),
443
+ - 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.smtp protocol.email io])),
444
+ - 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.pop protocol.email io])),
445
+ - 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.imap protocol.email io])),
446
+ - 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal provider.serialization])),
447
+ - 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml provider.serialization])),
448
+ - 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
449
+ - 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json provider.serialization])),
450
+ + 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGES.(%w[crypto.pkey])),
451
+ + 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGES.(%w[crypto.x509])),
452
+ + 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGES.(%w[crypto.pkcs5])),
453
+ + 'OpenSSL::Cipher' => [
454
+ + Hook.new(%i[encrypt], OPENSSL_PACKAGES.(%w[crypto.encrypt])),
455
+ + Hook.new(%i[decrypt], OPENSSL_PACKAGES.(%w[crypto.decrypt]))
456
+ + ],
457
+ + 'ActiveSupport::Callbacks::CallbackSequence' => [
458
+ + Hook.new(:invoke_before, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[mvc.before_action])),
459
+ + Hook.new(:invoke_after, Package.build_from_path('active_support', package_name: 'active_support', labels: %w[mvc.after_action])),
460
+ + ],
461
+ + 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGES.(%w[crypto.x509])),
462
+ + 'Net::HTTP' => Hook.new(:request, Package.build_from_path('net/http', package_name: 'net/http', labels: %w[protocol.http])),
463
+ + 'Net::SMTP' => Hook.new(:send, Package.build_from_path('net/smtp', package_name: 'net/smtp', labels: %w[protocol.email.smtp])),
464
+ + 'Net::POP3' => Hook.new(:mails, Package.build_from_path('net/pop3', package_name: 'net/pop', labels: %w[protocol.email.pop])),
465
+ + 'Net::IMAP' => Hook.new(:send_command, Package.build_from_path('net/imap', package_name: 'net/imap', labels: %w[protocol.email.imap])),
466
+ + 'Marshal' => Hook.new(%i[dump load], Package.build_from_path('marshal', labels: %w[format.marshal])),
467
+ + 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.build_from_path('yaml', package_name: 'psych', labels: %w[format.yaml])),
468
+ + 'JSON::Ext::Parser' => Hook.new(:parse, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
469
+ + 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.build_from_path('json', package_name: 'json', labels: %w[format.json])),
470
+ }.freeze
471
+
472
+ - attr_reader :name, :packages, :exclude
473
+ + attr_reader :name, :packages, :exclude, :builtin_methods
474
+
475
+ - def initialize(name, packages = [], exclude = [])
476
+ + def initialize(name, packages, exclude: [], functions: [])
477
+ @name = name
478
+ @packages = packages
479
+ @exclude = exclude
480
+ + @builtin_methods = BUILTIN_METHODS
481
+ + @functions = functions
482
+ + @hooked_methods = HOOKED_METHODS.dup
483
+ + functions.each do |func|
484
+ + package_options = {}
485
+ + package_options[:labels] = func.labels if func.labels
486
+ + @hooked_methods[func.cls] ||= []
487
+ + @hooked_methods[func.cls] << Hook.new(func.function_names, Package.build_from_path(func.package, package_options))
488
+ + end
489
+ end
490
+
491
+ class << self
492
+ @@ -98,6 +143,16 @@ module AppMap
493
+
494
+ # Loads configuration from a Hash.
495
+ def load(config_data)
496
+ + functions = (config_data['functions'] || []).map do |function_data|
497
+ + package = function_data['package']
498
+ + cls = function_data['class']
499
+ + functions = function_data['function'] || function_data['functions']
500
+ + raise 'AppMap class configuration should specify package, class and function(s)' unless package && cls && functions
501
+ + functions = Array(functions).map(&:to_sym)
502
+ + labels = function_data['label'] || function_data['labels']
503
+ + labels = Array(labels).map(&:to_s) if labels
504
+ + Function.new(package, cls, labels, functions)
505
+ + end
506
+ packages = (config_data['packages'] || []).map do |package|
507
+ gem = package['gem']
508
+ path = package['path']
509
+ @@ -112,7 +167,8 @@ module AppMap
510
+ Package.build_from_path(path, exclude: package['exclude'] || [], shallow: package['shallow'])
511
+ end
512
+ end.compact
513
+ - Config.new config_data['name'], packages, config_data['exclude'] || []
514
+ + exclude = config_data['exclude'] || []
515
+ + Config.new config_data['name'], packages, exclude: exclude, functions: functions
516
+ end
517
+ end
518
+
519
+ @@ -120,6 +176,7 @@ module AppMap
520
+ {
521
+ name: name,
522
+ packages: packages.map(&:to_h),
523
+ + functions: @functions.map(&:to_h),
524
+ exclude: exclude
525
+ }
526
+ end
527
+ @@ -164,14 +221,17 @@ module AppMap
528
+ end
529
+
530
+ def find_package(defined_class, method_name)
531
+ - hook = find_hook(defined_class)
532
+ - return nil unless hook
533
+ + hooks = find_hooks(defined_class)
534
+ + return nil unless hooks
535
+
536
+ - Array(hook.method_names).include?(method_name) ? hook.package : nil
537
+ + hook = Array(hooks).find do |hook|
538
+ + Array(hook.method_names).include?(method_name)
539
+ + end
540
+ + hook ? hook.package : nil
541
+ end
542
+
543
+ - def find_hook(defined_class)
544
+ - HOOKED_METHODS[defined_class] || BUILTIN_METHODS[defined_class]
545
+ + def find_hooks(defined_class)
546
+ + Array(@hooked_methods[defined_class] || @builtin_methods[defined_class])
547
+ end
548
+ end
549
+ end
550
+ diff --git a/lib/appmap/cucumber.rb b/lib/appmap/cucumber.rb
551
+ index 7b59c0a..eaf4ac3 100644
552
+ --- a/lib/appmap/cucumber.rb
553
+ +++ b/lib/appmap/cucumber.rb
554
+ @@ -50,7 +50,7 @@ module AppMap
555
+ appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
556
+ scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
557
+
558
+ - File.write(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
559
+ + AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
560
+ end
561
+
562
+ def enabled?
563
+ diff --git a/lib/appmap/hook.rb b/lib/appmap/hook.rb
564
+ index dd919d9..f141ac7 100644
565
+ --- a/lib/appmap/hook.rb
566
+ +++ b/lib/appmap/hook.rb
567
+ @@ -32,6 +32,7 @@ module AppMap
568
+ end
569
+
570
+ attr_reader :config
571
+ +
572
+ def initialize(config)
573
+ @config = config
574
+ end
575
+ @@ -59,6 +60,10 @@ module AppMap
576
+
577
+ hook = lambda do |hook_cls|
578
+ lambda do |method_id|
579
+ + # Don't try and trace the AppMap methods or there will be
580
+ + # a stack overflow in the defined hook method.
581
+ + return if (hook_cls&.name || '').split('::')[0] == AppMap.name
582
+ +
583
+ method = begin
584
+ hook_cls.public_instance_method(method_id)
585
+ rescue NameError
586
+ @@ -78,11 +83,9 @@ module AppMap
587
+ config.always_hook?(hook_cls, method.name) ||
588
+ config.included_by_location?(method)
589
+
590
+ - hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
591
+ + package = config.package_for_method(method)
592
+
593
+ - # Don't try and trace the AppMap methods or there will be
594
+ - # a stack overflow in the defined hook method.
595
+ - next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
596
+ + hook_method = Hook::Method.new(package, hook_cls, method)
597
+
598
+ hook_method.activate
599
+ end
600
+ @@ -112,25 +115,27 @@ module AppMap
601
+ end
602
+ end
603
+
604
+ - Config::BUILTIN_METHODS.each do |class_name, hook|
605
+ - require hook.package.package_name if hook.package.package_name
606
+ - Array(hook.method_names).each do |method_name|
607
+ - method_name = method_name.to_sym
608
+ + config.builtin_methods.each do |class_name, hooks|
609
+ + Array(hooks).each do |hook|
610
+ + require hook.package.package_name if hook.package.package_name
611
+ + Array(hook.method_names).each do |method_name|
612
+ + method_name = method_name.to_sym
613
+
614
+ - cls = class_from_string.(class_name)
615
+ - method = \
616
+ - begin
617
+ - cls.instance_method(method_name)
618
+ - rescue NameError
619
+ - cls.method(method_name) rescue nil
620
+ - end
621
+ + cls = class_from_string.(class_name)
622
+ + method = \
623
+ + begin
624
+ + cls.instance_method(method_name)
625
+ + rescue NameError
626
+ + cls.method(method_name) rescue nil
627
+ + end
628
+
629
+ - next if config.never_hook?(method)
630
+ + next if config.never_hook?(method)
631
+
632
+ - if method
633
+ - Hook::Method.new(hook.package, cls, method).activate
634
+ - else
635
+ - warn "Method #{method_name} not found on #{cls.name}"
636
+ + if method
637
+ + Hook::Method.new(hook.package, cls, method).activate
638
+ + else
639
+ + warn "Method #{method_name} not found on #{cls.name}"
640
+ + end
641
+ end
642
+ end
643
+ end
644
+ diff --git a/lib/appmap/middleware/remote_recording.rb b/lib/appmap/middleware/remote_recording.rb
645
+ index 83affc0..f695d25 100644
646
+ --- a/lib/appmap/middleware/remote_recording.rb
647
+ +++ b/lib/appmap/middleware/remote_recording.rb
648
+ @@ -67,7 +67,7 @@ module AppMap
649
+
650
+ response = JSON.generate \
651
+ version: AppMap::APPMAP_FORMAT_VERSION,
652
+ - classMap: AppMap.class_map(tracer.event_methods, include_source: AppMap.include_source?),
653
+ + classMap: AppMap.class_map(tracer.event_methods),
654
+ metadata: metadata,
655
+ events: @events
656
+
657
+ diff --git a/lib/appmap/minitest.rb b/lib/appmap/minitest.rb
658
+ index dc88bac..cf4a4d2 100644
659
+ --- a/lib/appmap/minitest.rb
660
+ +++ b/lib/appmap/minitest.rb
661
+ @@ -26,8 +26,9 @@ module AppMap
662
+ end
663
+
664
+
665
+ - def finish
666
+ + def finish(exception)
667
+ warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
668
+ + warn "Exception: #{exception}" if exception && AppMap::Minitest::LOG
669
+
670
+ events = []
671
+ AppMap.tracing.delete @trace
672
+ @@ -36,15 +37,17 @@ module AppMap
673
+
674
+ AppMap::Minitest.add_event_methods @trace.event_methods
675
+
676
+ - class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
677
+ + class_map = AppMap.class_map(@trace.event_methods)
678
+
679
+ feature_group = test.class.name.underscore.split('_')[0...-1].join('_').capitalize
680
+ feature_name = test.name.split('_')[1..-1].join(' ')
681
+ scenario_name = [ feature_group, feature_name ].join(' ')
682
+
683
+ - AppMap::Minitest.save scenario_name,
684
+ - class_map,
685
+ - source_location,
686
+ + AppMap::Minitest.save name: scenario_name,
687
+ + class_map: class_map,
688
+ + source_location: source_location,
689
+ + test_status: exception ? 'failed' : 'succeeded',
690
+ + exception: exception,
691
+ events: events
692
+ end
693
+ end
694
+ @@ -63,11 +66,11 @@ module AppMap
695
+ @recordings_by_test[test.object_id] = Recording.new(test, name)
696
+ end
697
+
698
+ - def end_test(test)
699
+ + def end_test(test, exception:)
700
+ recording = @recordings_by_test.delete(test.object_id)
701
+ return warn "No recording found for #{test}" unless recording
702
+
703
+ - recording.finish
704
+ + recording.finish exception
705
+ end
706
+
707
+ def config
708
+ @@ -78,9 +81,9 @@ module AppMap
709
+ @event_methods += event_methods
710
+ end
711
+
712
+ - def save(example_name, class_map, source_location, events: nil, labels: nil)
713
+ + def save(name:, class_map:, source_location:, test_status:, exception:, events:)
714
+ metadata = AppMap::Minitest.metadata.tap do |m|
715
+ - m[:name] = example_name
716
+ + m[:name] = name
717
+ m[:source_location] = source_location
718
+ m[:app] = AppMap.configuration.name
719
+ m[:frameworks] ||= []
720
+ @@ -91,6 +94,13 @@ module AppMap
721
+ m[:recorder] = {
722
+ name: 'minitest'
723
+ }
724
+ + m[:test_status] = test_status
725
+ + if exception
726
+ + m[:exception] = {
727
+ + class: exception.class.name,
728
+ + message: exception.to_s
729
+ + }
730
+ + end
731
+ end
732
+
733
+ appmap = {
734
+ @@ -99,14 +109,9 @@ module AppMap
735
+ classMap: class_map,
736
+ events: events
737
+ }.compact
738
+ - fname = AppMap::Util.scenario_filename(example_name)
739
+ + fname = AppMap::Util.scenario_filename(name)
740
+
741
+ - File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
742
+ - end
743
+ -
744
+ - def print_inventory
745
+ - class_map = AppMap.class_map(@event_methods)
746
+ - save 'Inventory', class_map, labels: %w[inventory]
747
+ + AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
748
+ end
749
+
750
+ def enabled?
751
+ @@ -115,9 +120,6 @@ module AppMap
752
+
753
+ def run
754
+ init
755
+ - at_exit do
756
+ - print_inventory
757
+ - end
758
+ end
759
+ end
760
+ end
761
+ @@ -135,7 +137,7 @@ if AppMap::Minitest.enabled?
762
+ begin
763
+ run_without_hook
764
+ ensure
765
+ - AppMap::Minitest.end_test self
766
+ + AppMap::Minitest.end_test self, exception: $!
767
+ end
768
+ end
769
+ end
770
+ diff --git a/lib/appmap/record.rb b/lib/appmap/record.rb
771
+ index f42da60..bb3ea94 100644
772
+ --- a/lib/appmap/record.rb
773
+ +++ b/lib/appmap/record.rb
774
+ @@ -23,5 +23,5 @@ at_exit do
775
+ 'classMap' => AppMap.class_map(tracer.event_methods),
776
+ 'events' => events
777
+ }
778
+ - File.write 'appmap.json', JSON.generate(appmap)
779
+ + AppMap::Util.write_appmap('appmap.json', JSON.generate(appmap))
780
+ end
781
+ diff --git a/lib/appmap/rspec.rb b/lib/appmap/rspec.rb
782
+ index bbc3781..79c0c66 100644
783
+ --- a/lib/appmap/rspec.rb
784
+ +++ b/lib/appmap/rspec.rb
785
+ @@ -94,8 +94,9 @@ module AppMap
786
+ result
787
+ end
788
+
789
+ - def finish
790
+ + def finish(exception)
791
+ warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
792
+ + warn "Exception: #{exception}" if exception && AppMap::RSpec::LOG
793
+
794
+ events = []
795
+ AppMap.tracing.delete @trace
796
+ @@ -104,7 +105,7 @@ module AppMap
797
+
798
+ AppMap::RSpec.add_event_methods @trace.event_methods
799
+
800
+ - class_map = AppMap.class_map(@trace.event_methods, include_source: AppMap.include_source?)
801
+ + class_map = AppMap.class_map(@trace.event_methods)
802
+
803
+ description = []
804
+ scope = ScopeExample.new(example)
805
+ @@ -127,9 +128,11 @@ module AppMap
806
+
807
+ full_description = normalize.call(description.join(' '))
808
+
809
+ - AppMap::RSpec.save full_description,
810
+ - class_map,
811
+ - source_location,
812
+ + AppMap::RSpec.save name: full_description,
813
+ + class_map: class_map,
814
+ + source_location: source_location,
815
+ + test_status: exception ? 'failed' : 'succeeded',
816
+ + exception: exception,
817
+ events: events
818
+ end
819
+ end
820
+ @@ -148,11 +151,11 @@ module AppMap
821
+ @recordings_by_example[example.object_id] = Recording.new(example)
822
+ end
823
+
824
+ - def end_spec(example)
825
+ + def end_spec(example, exception:)
826
+ recording = @recordings_by_example.delete(example.object_id)
827
+ return warn "No recording found for #{example}" unless recording
828
+
829
+ - recording.finish
830
+ + recording.finish exception
831
+ end
832
+
833
+ def config
834
+ @@ -163,12 +166,11 @@ module AppMap
835
+ @event_methods += event_methods
836
+ end
837
+
838
+ - def save(example_name, class_map, source_location, events: nil, labels: nil)
839
+ + def save(name:, class_map:, source_location:, test_status:, exception:, events:)
840
+ metadata = AppMap::RSpec.metadata.tap do |m|
841
+ - m[:name] = example_name
842
+ + m[:name] = name
843
+ m[:source_location] = source_location
844
+ m[:app] = AppMap.configuration.name
845
+ - m[:labels] = labels if labels
846
+ m[:frameworks] ||= []
847
+ m[:frameworks] << {
848
+ name: 'rspec',
849
+ @@ -177,6 +179,13 @@ module AppMap
850
+ m[:recorder] = {
851
+ name: 'rspec'
852
+ }
853
+ + m[:test_status] = test_status
854
+ + if exception
855
+ + m[:exception] = {
856
+ + class: exception.class.name,
857
+ + message: exception.to_s
858
+ + }
859
+ + end
860
+ end
861
+
862
+ appmap = {
863
+ @@ -185,14 +194,9 @@ module AppMap
864
+ classMap: class_map,
865
+ events: events
866
+ }.compact
867
+ - fname = AppMap::Util.scenario_filename(example_name)
868
+ -
869
+ - File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
870
+ - end
871
+ + fname = AppMap::Util.scenario_filename(name)
872
+
873
+ - def print_inventory
874
+ - class_map = AppMap.class_map(@event_methods)
875
+ - save 'Inventory', class_map, labels: %w[inventory]
876
+ + AppMap::Util.write_appmap(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
877
+ end
878
+
879
+ def enabled?
880
+ @@ -201,9 +205,6 @@ module AppMap
881
+
882
+ def run
883
+ init
884
+ - at_exit do
885
+ - print_inventory
886
+ - end
887
+ end
888
+ end
889
+ end
890
+ @@ -225,7 +226,7 @@ if AppMap::RSpec.enabled?
891
+ begin
892
+ instance_exec(&fn)
893
+ ensure
894
+ - AppMap::RSpec.end_spec example
895
+ + AppMap::RSpec.end_spec example, exception: $!
896
+ end
897
+ end
898
+ end
899
+ diff --git a/lib/appmap/util.rb b/lib/appmap/util.rb
900
+ index 133d54a..cd08ded 100644
901
+ --- a/lib/appmap/util.rb
902
+ +++ b/lib/appmap/util.rb
903
+ @@ -71,6 +71,22 @@ module AppMap
904
+
905
+ event
906
+ end
907
+ +
908
+ + # Atomically writes AppMap data to +filename+.
909
+ + def write_appmap(filename, appmap)
910
+ + require 'fileutils'
911
+ + require 'tmpdir'
912
+ +
913
+ + # This is what Ruby Tempfile does; but we don't want the file to be unlinked.
914
+ + mode = File::RDWR | File::CREAT | File::EXCL
915
+ + ::Dir::Tmpname.create([ 'appmap_', '.json' ]) do |tmpname|
916
+ + tempfile = File.open(tmpname, mode)
917
+ + tempfile.write(appmap)
918
+ + tempfile.close
919
+ + # Atomically move the tempfile into place.
920
+ + FileUtils.mv tempfile.path, filename
921
+ + end
922
+ + end
923
+ end
924
+ end
925
+ end
926
+ diff --git a/lib/appmap/version.rb b/lib/appmap/version.rb
927
+ index ff7d53b..8471d09 100644
928
+ --- a/lib/appmap/version.rb
929
+ +++ b/lib/appmap/version.rb
930
+ @@ -3,7 +3,7 @@
931
+ module AppMap
932
+ URL = 'https://github.com/applandinc/appmap-ruby'
933
+
934
+ - VERSION = '0.43.0'
935
+ + VERSION = '0.44.0'
936
+
937
+ APPMAP_FORMAT_VERSION = '1.4'
938
+ end
939
+ diff --git a/spec/abstract_controller_base_spec.rb b/spec/abstract_controller_base_spec.rb
940
+ index 26e58b1..2aed976 100644
941
+ --- a/spec/abstract_controller_base_spec.rb
942
+ +++ b/spec/abstract_controller_base_spec.rb
943
+ @@ -27,6 +27,8 @@ describe 'Rails' do
944
+ end
945
+
946
+ let(:appmap) { JSON.parse File.read File.join tmpdir, 'appmap/rspec', appmap_json_file }
947
+ + let(:appmap_json_path) { File.join(tmpdir, 'appmap/rspec', appmap_json_file) }
948
+ + let(:appmap) { JSON.parse File.read(appmap_json_path) }
949
+ let(:events) { appmap['events'] }
950
+
951
+ describe 'an API route' do
952
+ @@ -35,10 +37,6 @@ describe 'Rails' do
953
+ 'Api_UsersController_POST_api_users_with_required_parameters_creates_a_user.appmap.json'
954
+ end
955
+
956
+ - it 'inventory file is printed' do
957
+ - expect(File).to exist(File.join(tmpdir, 'appmap/rspec/Inventory.appmap.json'))
958
+ - end
959
+ -
960
+ it 'http_server_request is recorded in the appmap' do
961
+ expect(events).to include(
962
+ hash_including(
963
+ diff --git a/spec/class_map_spec.rb b/spec/class_map_spec.rb
964
+ index 343c8b9..4f60708 100644
965
+ --- a/spec/class_map_spec.rb
966
+ +++ b/spec/class_map_spec.rb
967
+ @@ -4,18 +4,10 @@ require 'spec_helper'
968
+
969
+ describe 'AppMap::ClassMap' do
970
+ describe '.build_from_methods' do
971
+ - it 'includes source code if available' do
972
+ - map = AppMap.class_map([scoped_method(method(:test_method))])
973
+ + it 'includes method comment' do
974
+ + map = AppMap.class_map([scoped_method((method :test_method))])
975
+ function = dig_map(map, 5)[0]
976
+ - expect(function[:source]).to include 'test method body'
977
+ - expect(function[:comment]).to include 'test method comment'
978
+ - end
979
+ -
980
+ - it 'can omit source code even if available' do
981
+ - map = AppMap.class_map([scoped_method((method :test_method))], include_source: false)
982
+ - function = dig_map(map, 5)[0]
983
+ - expect(function).to_not include(:source)
984
+ - expect(function).to_not include(:comment)
985
+ + expect(function).to include(:comment)
986
+ end
987
+
988
+ # test method comment
989
+ diff --git a/spec/config_spec.rb b/spec/config_spec.rb
990
+ index 5eeaac6..6657f1a 100644
991
+ --- a/spec/config_spec.rb
992
+ +++ b/spec/config_spec.rb
993
+ @@ -17,10 +17,40 @@ describe AppMap::Config, docker: false do
994
+ path: 'path-2',
995
+ exclude: [ 'exclude-1' ]
996
+ }
997
+ + ],
998
+ + functions: [
999
+ + {
1000
+ + package: 'pkg',
1001
+ + class: 'cls',
1002
+ + function: 'fn',
1003
+ + label: 'lbl'
1004
+ + }
1005
+ ]
1006
+ }.deep_stringify_keys!
1007
+ config = AppMap::Config.load(config_data)
1008
+
1009
+ - expect(config.to_h.deep_stringify_keys!).to eq(config_data)
1010
+ + config_expectation = {
1011
+ + exclude: [],
1012
+ + name: 'test',
1013
+ + packages: [
1014
+ + {
1015
+ + path: 'path-1'
1016
+ + },
1017
+ + {
1018
+ + path: 'path-2',
1019
+ + exclude: [ 'exclude-1' ]
1020
+ + }
1021
+ + ],
1022
+ + functions: [
1023
+ + {
1024
+ + package: 'pkg',
1025
+ + class: 'cls',
1026
+ + functions: [ :fn ],
1027
+ + labels: ['lbl']
1028
+ + }
1029
+ + ]
1030
+ + }.deep_stringify_keys!
1031
+ +
1032
+ + expect(config.to_h.deep_stringify_keys!).to eq(config_expectation)
1033
+ end
1034
+ end
1035
+ diff --git a/spec/fixtures/hook/custom_instance_method.rb b/spec/fixtures/hook/custom_instance_method.rb
1036
+ new file mode 100644
1037
+ index 0000000..285db81
1038
+ --- /dev/null
1039
+ +++ b/spec/fixtures/hook/custom_instance_method.rb
1040
+ @@ -0,0 +1,11 @@
1041
+ +# frozen_string_literal: true
1042
+ +
1043
+ +class CustomInstanceMethod
1044
+ + def to_s
1045
+ + 'CustomInstance Method fixture'
1046
+ + end
1047
+ +
1048
+ + def say_default
1049
+ + 'default'
1050
+ + end
1051
+ +end
1052
+ diff --git a/spec/fixtures/hook/method_named_call.rb b/spec/fixtures/hook/method_named_call.rb
1053
+ new file mode 100644
1054
+ index 0000000..69a2cc5
1055
+ --- /dev/null
1056
+ +++ b/spec/fixtures/hook/method_named_call.rb
1057
+ @@ -0,0 +1,11 @@
1058
+ +# frozen_string_literal: true
1059
+ +
1060
+ +class MethodNamedCall
1061
+ + def to_s
1062
+ + 'MethodNamedCall'
1063
+ + end
1064
+ +
1065
+ + def call(a, b, c, d, e)
1066
+ + [ a, b, c, d, e ].join(' ')
1067
+ + end
1068
+ +end
1069
+ diff --git a/spec/hook_spec.rb b/spec/hook_spec.rb
1070
+ index e4cbc99..8afb9f1 100644
1071
+ --- a/spec/hook_spec.rb
1072
+ +++ b/spec/hook_spec.rb
1073
+ @@ -64,13 +64,144 @@ describe 'AppMap class Hooking', docker: false do
1074
+ it 'excludes named classes and methods' do
1075
+ load 'spec/fixtures/hook/exclude.rb'
1076
+ package = AppMap::Config::Package.build_from_path('spec/fixtures/hook/exclude.rb')
1077
+ - config = AppMap::Config.new('hook_spec', [ package ], %w[ExcludeTest])
1078
+ + config = AppMap::Config.new('hook_spec', [ package ], exclude: %w[ExcludeTest])
1079
+ AppMap.configuration = config
1080
+
1081
+ expect(config.never_hook?(ExcludeTest.new.method(:instance_method))).to be_truthy
1082
+ expect(config.never_hook?(ExcludeTest.method(:cls_method))).to be_truthy
1083
+ end
1084
+
1085
+ + it "handles an instance method named 'call' without issues" do
1086
+ + events_yaml = <<~YAML
1087
+ + ---
1088
+ + - :id: 1
1089
+ + :event: :call
1090
+ + :defined_class: MethodNamedCall
1091
+ + :method_id: call
1092
+ + :path: spec/fixtures/hook/method_named_call.rb
1093
+ + :lineno: 8
1094
+ + :static: false
1095
+ + :parameters:
1096
+ + - :name: :a
1097
+ + :class: Integer
1098
+ + :value: '1'
1099
+ + :kind: :req
1100
+ + - :name: :b
1101
+ + :class: Integer
1102
+ + :value: '2'
1103
+ + :kind: :req
1104
+ + - :name: :c
1105
+ + :class: Integer
1106
+ + :value: '3'
1107
+ + :kind: :req
1108
+ + - :name: :d
1109
+ + :class: Integer
1110
+ + :value: '4'
1111
+ + :kind: :req
1112
+ + - :name: :e
1113
+ + :class: Integer
1114
+ + :value: '5'
1115
+ + :kind: :req
1116
+ + :receiver:
1117
+ + :class: MethodNamedCall
1118
+ + :value: MethodNamedCall
1119
+ + - :id: 2
1120
+ + :event: :return
1121
+ + :parent_id: 1
1122
+ + :return_value:
1123
+ + :class: String
1124
+ + :value: 1 2 3 4 5
1125
+ + YAML
1126
+ +
1127
+ + _, tracer = test_hook_behavior 'spec/fixtures/hook/method_named_call.rb', events_yaml do
1128
+ + expect(MethodNamedCall.new.call(1, 2, 3, 4, 5)).to eq('1 2 3 4 5')
1129
+ + end
1130
+ + class_map = AppMap.class_map(tracer.event_methods)
1131
+ + expect(Diffy::Diff.new(<<~CLASSMAP, YAML.dump(class_map)).to_s).to eq('')
1132
+ + ---
1133
+ + - :name: spec/fixtures/hook/method_named_call.rb
1134
+ + :type: package
1135
+ + :children:
1136
+ + - :name: MethodNamedCall
1137
+ + :type: class
1138
+ + :children:
1139
+ + - :name: call
1140
+ + :type: function
1141
+ + :location: spec/fixtures/hook/method_named_call.rb:8
1142
+ + :static: false
1143
+ + CLASSMAP
1144
+ + end
1145
+ +
1146
+ + it 'can custom hook and label a function' do
1147
+ + events_yaml = <<~YAML
1148
+ + ---
1149
+ + - :id: 1
1150
+ + :event: :call
1151
+ + :defined_class: CustomInstanceMethod
1152
+ + :method_id: say_default
1153
+ + :path: spec/fixtures/hook/custom_instance_method.rb
1154
+ + :lineno: 8
1155
+ + :static: false
1156
+ + :parameters: []
1157
+ + :receiver:
1158
+ + :class: CustomInstanceMethod
1159
+ + :value: CustomInstance Method fixture
1160
+ + - :id: 2
1161
+ + :event: :return
1162
+ + :parent_id: 1
1163
+ + :return_value:
1164
+ + :class: String
1165
+ + :value: default
1166
+ + YAML
1167
+ +
1168
+ + config = AppMap::Config.load({
1169
+ + functions: [
1170
+ + {
1171
+ + package: 'hook_spec',
1172
+ + class: 'CustomInstanceMethod',
1173
+ + functions: [ :say_default ],
1174
+ + labels: ['cowsay']
1175
+ + }
1176
+ + ]
1177
+ + }.deep_stringify_keys)
1178
+ +
1179
+ + load 'spec/fixtures/hook/custom_instance_method.rb'
1180
+ + hook_cls = CustomInstanceMethod
1181
+ + method = hook_cls.instance_method(:say_default)
1182
+ +
1183
+ + require 'appmap/hook/method'
1184
+ + hook_method = AppMap::Hook::Method.new(config.package_for_method(method), hook_cls, method)
1185
+ + hook_method.activate
1186
+ +
1187
+ + tracer = AppMap.tracing.trace
1188
+ + AppMap::Event.reset_id_counter
1189
+ + begin
1190
+ + expect(CustomInstanceMethod.new.say_default).to eq('default')
1191
+ + ensure
1192
+ + AppMap.tracing.delete(tracer)
1193
+ + end
1194
+ +
1195
+ + events = collect_events(tracer).to_yaml
1196
+ +
1197
+ + expect(Diffy::Diff.new(events_yaml, events).to_s).to eq('')
1198
+ + class_map = AppMap.class_map(tracer.event_methods)
1199
+ + expect(Diffy::Diff.new(<<~CLASSMAP, YAML.dump(class_map)).to_s).to eq('')
1200
+ + ---
1201
+ + - :name: hook_spec
1202
+ + :type: package
1203
+ + :children:
1204
+ + - :name: CustomInstanceMethod
1205
+ + :type: class
1206
+ + :children:
1207
+ + - :name: say_default
1208
+ + :type: function
1209
+ + :location: spec/fixtures/hook/custom_instance_method.rb:8
1210
+ + :static: false
1211
+ + :labels:
1212
+ + - cowsay
1213
+ + CLASSMAP
1214
+ + end
1215
+ +
1216
+ it 'parses labels from comments' do
1217
+ _, tracer = invoke_test_file 'spec/fixtures/hook/labels.rb' do
1218
+ ClassWithLabel.new.fn_with_label
1219
+ @@ -91,9 +222,6 @@ describe 'AppMap class Hooking', docker: false do
1220
+ :labels:
1221
+ - has-fn-label
1222
+ :comment: "# @label has-fn-label\\n"
1223
+ - :source: |2
1224
+ - def fn_with_label
1225
+ - end
1226
+ YAML
1227
+ end
1228
+
1229
+ @@ -148,10 +276,6 @@ describe 'AppMap class Hooking', docker: false do
1230
+ :type: function
1231
+ :location: spec/fixtures/hook/instance_method.rb:8
1232
+ :static: false
1233
+ - :source: |2
1234
+ - def say_default
1235
+ - 'default'
1236
+ - end
1237
+ YAML
1238
+ end
1239
+
1240
+ @@ -746,6 +870,7 @@ describe 'AppMap class Hooking', docker: false do
1241
+ end
1242
+ secure_compare_event = YAML.load(events).find { |evt| evt[:defined_class] == 'ActiveSupport::SecurityUtils' }
1243
+ secure_compare_event.delete(:lineno)
1244
+ + secure_compare_event.delete(:path)
1245
+
1246
+ expect(Diffy::Diff.new(<<~YAML, secure_compare_event.to_yaml).to_s).to eq('')
1247
+ ---
1248
+ @@ -753,7 +878,6 @@ describe 'AppMap class Hooking', docker: false do
1249
+ :event: :call
1250
+ :defined_class: ActiveSupport::SecurityUtils
1251
+ :method_id: secure_compare
1252
+ - :path: lib/active_support/security_utils.rb
1253
+ :static: true
1254
+ :parameters:
1255
+ - :name: :a
1256
+ @@ -837,7 +961,7 @@ describe 'AppMap class Hooking', docker: false do
1257
+ entry = cm[1][:children][0][:children][0][:children][0]
1258
+ # Sanity check, make sure we got the right one
1259
+ expect(entry[:name]).to eq('secure_compare')
1260
+ - expect(entry[:labels]).to eq(%w[provider.secure_compare])
1261
+ + expect(entry[:labels]).to eq(%w[crypto.secure_compare])
1262
+ end
1263
+ end
1264
+
1265
+ diff --git a/test/cli_test.rb b/test/cli_test.rb
1266
+ deleted file mode 100755
1267
+ index 2ea654c..0000000
1268
+ --- a/test/cli_test.rb
1269
+ +++ /dev/null
1270
+ @@ -1,116 +0,0 @@
1271
+ -#!/usr/bin/env ruby
1272
+ -# frozen_string_literal: true
1273
+ -
1274
+ -require 'test_helper'
1275
+ -require 'English'
1276
+ -
1277
+ -class CLITest < Minitest::Test
1278
+ - OUTPUT_FILENAME = File.expand_path('../tmp/appmap.json', __dir__)
1279
+ - STATS_OUTPUT_FILENAME = File.expand_path('../tmp/stats.txt', __dir__)
1280
+ -
1281
+ - def setup
1282
+ - FileUtils.rm_f OUTPUT_FILENAME
1283
+ - FileUtils.rm_f STATS_OUTPUT_FILENAME
1284
+ - end
1285
+ -
1286
+ - def test_record
1287
+ - output = Dir.chdir 'test/fixtures/cli_record_test' do
1288
+ - `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`.strip
1289
+ - end
1290
+ -
1291
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1292
+ - assert File.file?(OUTPUT_FILENAME), "#{OUTPUT_FILENAME} does not exist"
1293
+ - assert_equal 'Hello', output
1294
+ - output = JSON.parse(File.read(OUTPUT_FILENAME))
1295
+ - assert output['classMap'], 'Output should contain classMap'
1296
+ - assert output['events'], 'Output should contain events'
1297
+ - end
1298
+ -
1299
+ - def test_stats_to_file
1300
+ - Dir.chdir 'test/fixtures/cli_record_test' do
1301
+ - `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`.strip
1302
+ - end
1303
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1304
+ -
1305
+ - output = Dir.chdir 'test/fixtures/cli_record_test' do
1306
+ - `#{File.expand_path '../exe/appmap', __dir__} stats -o #{STATS_OUTPUT_FILENAME} #{OUTPUT_FILENAME}`.strip
1307
+ - end
1308
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1309
+ - assert_equal '', output
1310
+ - assert File.file?(OUTPUT_FILENAME), "#{OUTPUT_FILENAME} does not exist"
1311
+ - end
1312
+ -
1313
+ -
1314
+ - def test_stats_text
1315
+ - Dir.chdir 'test/fixtures/cli_record_test' do
1316
+ - `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`.strip
1317
+ - end
1318
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1319
+ -
1320
+ - output = Dir.chdir 'test/fixtures/cli_record_test' do
1321
+ - `#{File.expand_path '../exe/appmap', __dir__} stats -o - #{OUTPUT_FILENAME}`.strip
1322
+ - end
1323
+ -
1324
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1325
+ - assert_equal <<~OUTPUT.strip, output.strip
1326
+ - Class frequency:
1327
+ - ----------------
1328
+ - 1 Main
1329
+ -
1330
+ - Method frequency:
1331
+ - ----------------
1332
+ - 1 Main.say_hello
1333
+ - OUTPUT
1334
+ - end
1335
+ -
1336
+ - def test_stats_json
1337
+ - Dir.chdir 'test/fixtures/cli_record_test' do
1338
+ - `#{File.expand_path '../exe/appmap', __dir__} record -o #{OUTPUT_FILENAME} ./lib/cli_record_test/main.rb`.strip
1339
+ - end
1340
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1341
+ -
1342
+ - output = Dir.chdir 'test/fixtures/cli_record_test' do
1343
+ - `#{File.expand_path '../exe/appmap', __dir__} stats -f json -o - #{OUTPUT_FILENAME}`.strip
1344
+ - end
1345
+ -
1346
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1347
+ - assert_equal <<~OUTPUT.strip, output.strip
1348
+ - {
1349
+ - "class_frequency": [
1350
+ - {
1351
+ - "name": "Main",
1352
+ - "count": 1
1353
+ - }
1354
+ - ],
1355
+ - "method_frequency": [
1356
+ - {
1357
+ - "name": "Main.say_hello",
1358
+ - "count": 1
1359
+ - }
1360
+ - ]
1361
+ - }
1362
+ - OUTPUT
1363
+ - end
1364
+ -
1365
+ - def test_record_to_default_location
1366
+ - Dir.chdir 'test/fixtures/cli_record_test' do
1367
+ - system({ 'APPMAP_FILE' => OUTPUT_FILENAME }, "#{File.expand_path '../exe/appmap', __dir__} record ./lib/cli_record_test/main.rb")
1368
+ - end
1369
+ -
1370
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1371
+ - assert File.file?(OUTPUT_FILENAME), 'appmap.json does not exist'
1372
+ - end
1373
+ -
1374
+ - def test_record_to_stdout
1375
+ - output = Dir.chdir 'test/fixtures/cli_record_test' do
1376
+ - `#{File.expand_path '../exe/appmap', __dir__} record -o - ./lib/cli_record_test/main.rb`
1377
+ - end
1378
+ -
1379
+ - assert_equal 0, $CHILD_STATUS.exitstatus
1380
+ - # Event path
1381
+ - assert_includes output, %("path":"lib/cli_record_test/main.rb")
1382
+ - # Function location
1383
+ - assert_includes output, %("location":"lib/cli_record_test/main.rb:3")
1384
+ - assert !File.file?(OUTPUT_FILENAME), "#{OUTPUT_FILENAME} should not exist"
1385
+ - end
1386
+ -end
1387
+ diff --git a/test/expectations/openssl_test_key_sign1.json b/test/expectations/openssl_test_key_sign1.json
1388
+ index 6489b47..0613a69 100644
1389
+ --- a/test/expectations/openssl_test_key_sign1.json
1390
+ +++ b/test/expectations/openssl_test_key_sign1.json
1391
+ @@ -11,8 +11,7 @@
1392
+ "name": "sign",
1393
+ "type": "function",
1394
+ "location": "lib/openssl_key_sign.rb:10",
1395
+ - "static": true,
1396
+ - "source": " def Example.sign\n key = OpenSSL::PKey::RSA.new 2048\n\n document = 'the document'\n\n digest = OpenSSL::Digest::SHA256.new\n key.sign digest, document\n end\n"
1397
+ + "static": true
1398
+ }
1399
+ ]
1400
+ }
1401
+ @@ -40,8 +39,7 @@
1402
+ "location": "OpenSSL::PKey::PKey#sign",
1403
+ "static": false,
1404
+ "labels": [
1405
+ - "security",
1406
+ - "crypto"
1407
+ + "crypto.pkey"
1408
+ ]
1409
+ }
1410
+ ]
1411
+ diff --git a/test/gem_test.rb b/test/gem_test.rb
1412
+ index 6cae910..1342c2a 100644
1413
+ --- a/test/gem_test.rb
1414
+ +++ b/test/gem_test.rb
1415
+ @@ -26,7 +26,7 @@ class MinitestTest < Minitest::Test
1416
+ assert_equal 2, events.size
1417
+ assert_equal 'call', events.first['event']
1418
+ assert_equal 'default_parser', events.first['method_id']
1419
+ - assert_equal "#{Gem.loaded_specs['parser'].gem_dir}/lib/parser/base.rb", events.first['path']
1420
+ + assert_match /\lib\/parser\/base\.rb$/, events.first['path']
1421
+ assert_equal 'return', events.second['event']
1422
+ assert_equal 1, events.second['parent_id']
1423
+ end
1424
+ diff --git a/test/rspec_test.rb b/test/rspec_test.rb
1425
+ index 72ed029..b30618b 100644
1426
+ --- a/test/rspec_test.rb
1427
+ +++ b/test/rspec_test.rb
1428
+ @@ -18,19 +18,6 @@ class RSpecTest < Minitest::Test
1429
+ end
1430
+ end
1431
+
1432
+ - def test_inventory
1433
+ - perform_test 'plain_hello_spec' do
1434
+ - appmap_file = 'tmp/appmap/rspec/Inventory.appmap.json'
1435
+ -
1436
+ - assert File.file?(appmap_file), 'appmap output file does not exist'
1437
+ - appmap = JSON.parse(File.read(appmap_file))
1438
+ - assert_equal AppMap::APPMAP_FORMAT_VERSION, appmap['version']
1439
+ - assert_includes appmap.keys, 'metadata'
1440
+ - metadata = appmap['metadata']
1441
+ - assert_equal 'Inventory', metadata['name']
1442
+ - end
1443
+ - end
1444
+ -
1445
+ def test_record_decorated_rspec
1446
+ perform_test 'decorated_hello_spec' do
1447
+ appmap_file = 'tmp/appmap/rspec/Hello_says_hello.appmap.json'