appmap 0.26.1 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -3
  3. data/CHANGELOG.md +37 -0
  4. data/README.md +170 -29
  5. data/Rakefile +1 -1
  6. data/exe/appmap +3 -1
  7. data/lib/appmap.rb +56 -35
  8. data/lib/appmap/algorithm/stats.rb +2 -1
  9. data/lib/appmap/class_map.rb +21 -28
  10. data/lib/appmap/command/record.rb +2 -61
  11. data/lib/appmap/config.rb +89 -0
  12. data/lib/appmap/cucumber.rb +89 -0
  13. data/lib/appmap/event.rb +28 -19
  14. data/lib/appmap/hook.rb +56 -128
  15. data/lib/appmap/hook/method.rb +78 -0
  16. data/lib/appmap/metadata.rb +62 -0
  17. data/lib/appmap/middleware/remote_recording.rb +2 -6
  18. data/lib/appmap/minitest.rb +141 -0
  19. data/lib/appmap/rails/action_handler.rb +7 -7
  20. data/lib/appmap/rails/sql_handler.rb +10 -8
  21. data/lib/appmap/railtie.rb +2 -2
  22. data/lib/appmap/record.rb +27 -0
  23. data/lib/appmap/rspec.rb +20 -38
  24. data/lib/appmap/trace.rb +19 -11
  25. data/lib/appmap/util.rb +59 -0
  26. data/lib/appmap/version.rb +1 -1
  27. data/package-lock.json +3 -3
  28. data/spec/abstract_controller4_base_spec.rb +1 -1
  29. data/spec/abstract_controller_base_spec.rb +9 -2
  30. data/spec/config_spec.rb +3 -3
  31. data/spec/fixtures/hook/compare.rb +7 -0
  32. data/spec/fixtures/hook/singleton_method.rb +54 -0
  33. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  34. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  35. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  36. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  37. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  38. data/spec/hook_spec.rb +228 -53
  39. data/spec/rails_spec_helper.rb +2 -0
  40. data/spec/record_sql_rails_pg_spec.rb +56 -33
  41. data/spec/rspec_feature_metadata_spec.rb +2 -0
  42. data/spec/spec_helper.rb +4 -0
  43. data/spec/util_spec.rb +21 -0
  44. data/test/cli_test.rb +4 -4
  45. data/test/cucumber_test.rb +72 -0
  46. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  47. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  48. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  49. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  50. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  51. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  52. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  53. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  54. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  55. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  56. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  57. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  58. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  59. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  60. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  61. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  62. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  63. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  64. data/test/fixtures/process_recorder/appmap.yml +3 -0
  65. data/test/fixtures/process_recorder/hello.rb +9 -0
  66. data/test/minitest_test.rb +38 -0
  67. data/test/record_process_test.rb +35 -0
  68. data/test/test_helper.rb +1 -0
  69. metadata +39 -3
  70. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -16,11 +16,11 @@ module AppMap
16
16
  class HTTPServerRequest
17
17
  include ContextKey
18
18
 
19
- class Call < AppMap::Event::MethodEvent
19
+ class Call < AppMap::Event::MethodCall
20
20
  attr_accessor :payload
21
21
 
22
- def initialize(path, lineno, payload)
23
- super AppMap::Event.next_id_counter, :call, HTTPServerRequest, :call, path, lineno, false, Thread.current.object_id
22
+ def initialize(payload)
23
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
24
24
 
25
25
  self.payload = payload
26
26
  end
@@ -47,7 +47,7 @@ module AppMap
47
47
  end
48
48
 
49
49
  def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
50
- event = Call.new(__FILE__, __LINE__, payload)
50
+ event = Call.new(payload)
51
51
  Thread.current[context_key] = Context.new(event.id, Time.now)
52
52
  AppMap.tracing.record_event(event)
53
53
  end
@@ -59,8 +59,8 @@ module AppMap
59
59
  class Call < AppMap::Event::MethodReturnIgnoreValue
60
60
  attr_accessor :payload
61
61
 
62
- def initialize(path, lineno, payload, parent_id, elapsed)
63
- super AppMap::Event.next_id_counter, :return, HTTPServerResponse, :call, path, lineno, false, Thread.current.object_id
62
+ def initialize(payload, parent_id, elapsed)
63
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
64
64
 
65
65
  self.payload = payload
66
66
  self.parent_id = parent_id
@@ -82,7 +82,7 @@ module AppMap
82
82
  context = Thread.current[context_key]
83
83
  Thread.current[context_key] = nil
84
84
 
85
- event = Call.new(__FILE__, __LINE__, payload, context.id, Time.now - context.start_time)
85
+ event = Call.new(payload, context.id, Time.now - context.start_time)
86
86
  AppMap.tracing.record_event(event)
87
87
  end
88
88
  end
@@ -5,11 +5,11 @@ require 'appmap/event'
5
5
  module AppMap
6
6
  module Rails
7
7
  class SQLHandler
8
- class SQLCall < AppMap::Event::MethodEvent
8
+ class SQLCall < AppMap::Event::MethodCall
9
9
  attr_accessor :payload
10
10
 
11
- def initialize(path, lineno, payload)
12
- super AppMap::Event.next_id_counter, :call, SQLHandler, :call, path, lineno, false, Thread.current.object_id
11
+ def initialize(payload)
12
+ super AppMap::Event.next_id_counter, :call, Thread.current.object_id
13
13
 
14
14
  self.payload = payload
15
15
  end
@@ -20,7 +20,7 @@ module AppMap
20
20
  sql: payload[:sql],
21
21
  database_type: payload[:database_type]
22
22
  }.tap do |sql_query|
23
- %i[server_version explain_sql].each do |attribute|
23
+ %i[server_version].each do |attribute|
24
24
  sql_query[attribute] = payload[attribute] if payload[attribute]
25
25
  end
26
26
  end
@@ -29,8 +29,8 @@ module AppMap
29
29
  end
30
30
 
31
31
  class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
32
- def initialize(path, lineno, parent_id, elapsed)
33
- super AppMap::Event.next_id_counter, :return, SQLHandler, :call, path, lineno, false, Thread.current.object_id
32
+ def initialize(parent_id, elapsed)
33
+ super AppMap::Event.next_id_counter, :return, Thread.current.object_id
34
34
 
35
35
  self.parent_id = parent_id
36
36
  self.elapsed = elapsed
@@ -76,6 +76,8 @@ module AppMap
76
76
  case database_type
77
77
  when :postgres
78
78
  ActiveRecord::Base.connection.postgresql_version
79
+ when :sqlite
80
+ ActiveRecord::Base.connection.database_version.to_s
79
81
  else
80
82
  warn "Unable to determine database version for #{database_type.inspect}"
81
83
  end
@@ -133,9 +135,9 @@ module AppMap
133
135
 
134
136
  SQLExaminer.examine payload, sql: sql
135
137
 
136
- call = SQLCall.new(__FILE__, __LINE__, payload)
138
+ call = SQLCall.new(payload)
137
139
  AppMap.tracing.record_event(call)
138
- AppMap.tracing.record_event(SQLReturn.new(__FILE__, __LINE__, call.id, finished - started))
140
+ AppMap.tracing.record_event(SQLReturn.new(call.id, finished - started))
139
141
  ensure
140
142
  Thread.current[reentry_key] = nil
141
143
  end
@@ -5,8 +5,8 @@ module AppMap
5
5
  class Railtie < ::Rails::Railtie
6
6
  config.appmap = ActiveSupport::OrderedOptions.new
7
7
 
8
- initializer 'appmap.init' do |_| # params: app
9
- AppMap.configure
8
+ initializer 'appmap.init' do |_| # params: app
9
+ require 'appmap'
10
10
  end
11
11
 
12
12
  # appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap'
4
+ require 'json'
5
+
6
+ tracer = AppMap.tracing.trace
7
+
8
+ at_exit do
9
+ AppMap.tracing.delete(tracer)
10
+
11
+ events = [].tap do |event_list|
12
+ event_list << tracer.next_event.to_h while tracer.event?
13
+ end
14
+
15
+ metadata = AppMap.detect_metadata
16
+ metadata[:recorder] = {
17
+ name: 'record_process'
18
+ }
19
+
20
+ appmap = {
21
+ 'version' => AppMap::APPMAP_FORMAT_VERSION,
22
+ 'metadata' => metadata,
23
+ 'classMap' => AppMap.class_map(tracer.event_methods),
24
+ 'events' => events
25
+ }
26
+ File.write 'appmap.json', JSON.generate(appmap)
27
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'appmap/util'
4
+
3
5
  module AppMap
4
6
  # Integration of AppMap with RSpec. When enabled with APPMAP=true, the AppMap tracer will
5
7
  # be activated around each scenario which has the metadata key `:appmap`.
@@ -8,10 +10,7 @@ module AppMap
8
10
  LOG = false
9
11
 
10
12
  def self.metadata
11
- require 'appmap/command/record'
12
- @metadata ||= AppMap::Command::Record.detect_metadata
13
- @metadata.freeze
14
- @metadata.deep_dup
13
+ AppMap.detect_metadata
15
14
  end
16
15
 
17
16
  module FeatureAnnotations
@@ -107,7 +106,17 @@ module AppMap
107
106
 
108
107
  def parent
109
108
  # An example group always has a parent; but it might be 'self'...
110
- example_group.parent != example_group ? ScopeExampleGroup.new(example_group.parent) : nil
109
+
110
+ # DEPRECATION WARNING: `Module#parent` has been renamed to `module_parent`. `parent` is deprecated and will be
111
+ # removed in Rails 6.1. (called from parent at /Users/kgilpin/source/appland/appmap-ruby/lib/appmap/rspec.rb:110)
112
+ example_group_parent = \
113
+ if example_group.respond_to?(:module_parent)
114
+ example_group.module_parent
115
+ else
116
+ example_group.parent
117
+ end
118
+
119
+ example_group_parent != example_group ? ScopeExampleGroup.new(example_group_parent) : nil
111
120
  end
112
121
  end
113
122
 
@@ -129,7 +138,7 @@ module AppMap
129
138
 
130
139
  AppMap::RSpec.add_event_methods @trace.event_methods
131
140
 
132
- class_map = AppMap.class_map(AppMap::RSpec.config, @trace.event_methods)
141
+ class_map = AppMap.class_map(@trace.event_methods)
133
142
 
134
143
  description = []
135
144
  scope = ScopeExample.new(example)
@@ -180,7 +189,6 @@ module AppMap
180
189
  end
181
190
 
182
191
  @recordings_by_example = {}
183
- @config = nil
184
192
  @event_methods = Set.new
185
193
 
186
194
  class << self
@@ -188,10 +196,6 @@ module AppMap
188
196
  warn 'Configuring AppMap recorder for RSpec'
189
197
 
190
198
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
191
-
192
- require 'appmap/hook'
193
- @config = AppMap.configure
194
- AppMap::Hook.hook(@config)
195
199
  end
196
200
 
197
201
  def begin_spec(example)
@@ -214,9 +218,9 @@ module AppMap
214
218
  end
215
219
 
216
220
  def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
217
- metadata = RSpec.metadata.tap do |m|
221
+ metadata = AppMap::RSpec.metadata.tap do |m|
218
222
  m[:name] = example_name
219
- m[:app] = @config.name
223
+ m[:app] = AppMap.configuration.name
220
224
  m[:feature] = feature_name if feature_name
221
225
  m[:feature_group] = feature_group_name if feature_group_name
222
226
  m[:labels] = labels if labels
@@ -236,13 +240,13 @@ module AppMap
236
240
  classMap: class_map,
237
241
  events: events
238
242
  }.compact
239
- fname = sanitize_filename(example_name)
243
+ fname = AppMap::Util.scenario_filename(example_name)
240
244
 
241
- File.write(File.join(APPMAP_OUTPUT_DIR, "#{fname}.appmap.json"), JSON.generate(appmap))
245
+ File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
242
246
  end
243
247
 
244
248
  def print_inventory
245
- class_map = AppMap.class_map(@config, @event_methods)
249
+ class_map = AppMap.class_map(@event_methods)
246
250
  save 'Inventory', class_map, labels: %w[inventory]
247
251
  end
248
252
 
@@ -256,28 +260,6 @@ module AppMap
256
260
  print_inventory
257
261
  end
258
262
  end
259
-
260
- private
261
-
262
- # Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
263
- # https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
264
- def sanitize_filename(fname, separator: '_')
265
- # Replace accented chars with their ASCII equivalents.
266
- fname = fname.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
267
-
268
- # Turn unwanted chars into the separator.
269
- fname.gsub!(/[^a-z0-9\-_]+/i, separator)
270
-
271
- re_sep = Regexp.escape(separator)
272
- re_duplicate_separator = /#{re_sep}{2,}/
273
- re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
274
-
275
- # No more than one of the separator in a row.
276
- fname.gsub!(re_duplicate_separator, separator)
277
-
278
- # Finally, Remove leading/trailing separator.
279
- fname.gsub(re_leading_trailing_separator, '')
280
- end
281
263
  end
282
264
  end
283
265
  end
@@ -2,38 +2,46 @@
2
2
 
3
3
  module AppMap
4
4
  module Trace
5
- ScopedMethod = Struct.new(:defined_class, :method)
5
+ class ScopedMethod < SimpleDelegator
6
+ attr_reader :defined_class, :static
7
+
8
+ def initialize(defined_class, method, static)
9
+ @defined_class = defined_class
10
+ @static = static
11
+ super(method)
12
+ end
13
+ end
6
14
 
7
- class Tracers
15
+ class Tracing
8
16
  def initialize
9
- @tracers = []
17
+ @tracing = []
10
18
  end
11
19
 
12
20
  def empty?
13
- @tracers.empty?
21
+ @tracing.empty?
14
22
  end
15
23
 
16
24
  def trace(enable: true)
17
25
  Tracer.new.tap do |tracer|
18
- @tracers << tracer
26
+ @tracing << tracer
19
27
  tracer.enable if enable
20
28
  end
21
- end
29
+ end
22
30
 
23
31
  def enabled?
24
- @tracers.any?(&:enabled?)
32
+ @tracing.any?(&:enabled?)
25
33
  end
26
34
 
27
35
  def record_event(event, defined_class: nil, method: nil)
28
- @tracers.each do |tracer|
36
+ @tracing.each do |tracer|
29
37
  tracer.record_event(event, defined_class: defined_class, method: method)
30
38
  end
31
39
  end
32
40
 
33
41
  def delete(tracer)
34
- return unless @tracers.member?(tracer)
42
+ return unless @tracing.member?(tracer)
35
43
 
36
- @tracers.delete(tracer)
44
+ @tracing.delete(tracer)
37
45
  tracer.disable
38
46
  end
39
47
  end
@@ -67,7 +75,7 @@ module AppMap
67
75
  return unless @enabled
68
76
 
69
77
  @events << event
70
- @methods << Trace::ScopedMethod.new(defined_class, method) if defined_class && method
78
+ @methods << Trace::ScopedMethod.new(defined_class, method, event.static) if (defined_class && method && event.event == :call)
71
79
  end
72
80
 
73
81
  # Gets a unique list of the methods that were invoked by the program.
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ module Util
5
+ class << self
6
+ # scenario_filename builds a suitable file name from a scenario name.
7
+ # Special characters are removed, and the file name is truncated to fit within
8
+ # shell limitations.
9
+ def scenario_filename(name, max_length: 255, separator: '_', extension: '.appmap.json')
10
+ # Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
11
+ # https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
12
+ # Replace accented chars with their ASCII equivalents.
13
+
14
+ fname = name.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
15
+
16
+ # Turn unwanted chars into the separator.
17
+ fname.gsub!(/[^a-z0-9\-_]+/i, separator)
18
+
19
+ re_sep = Regexp.escape(separator)
20
+ re_duplicate_separator = /#{re_sep}{2,}/
21
+ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
22
+
23
+ # No more than one of the separator in a row.
24
+ fname.gsub!(re_duplicate_separator, separator)
25
+
26
+ # Finally, Remove leading/trailing separator.
27
+ fname.gsub!(re_leading_trailing_separator, '')
28
+
29
+ if (fname.length + extension.length) > max_length
30
+ require 'base64'
31
+ require 'digest'
32
+ fname_digest = Base64.urlsafe_encode64 Digest::MD5.digest(fname), padding: false
33
+ fname[max_length - fname_digest.length - extension.length - 1..-1] = [ '-', fname_digest ].join
34
+ end
35
+
36
+ [ fname, extension ].join
37
+ end
38
+
39
+ # sanitize_event removes ephemeral values from an event, making
40
+ # events easier to compare across runs.
41
+ def sanitize_event(event, &block)
42
+ event.delete(:thread_id)
43
+ event.delete(:elapsed)
44
+ delete_object_id = ->(obj) { (obj || {}).delete(:object_id) }
45
+ delete_object_id.call(event[:receiver])
46
+ delete_object_id.call(event[:return_value])
47
+ (event[:parameters] || []).each(&delete_object_id)
48
+ (event[:exceptions] || []).each(&delete_object_id)
49
+
50
+ case event[:event]
51
+ when :call
52
+ event[:path] = event[:path].gsub(Gem.dir + '/', '')
53
+ end
54
+
55
+ event
56
+ end
57
+ end
58
+ end
59
+ end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.26.1'
6
+ VERSION = '0.32.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -551,9 +551,9 @@
551
551
  }
552
552
  },
553
553
  "lodash": {
554
- "version": "4.17.15",
555
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
556
- "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
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=="
557
557
  },
558
558
  "longest": {
559
559
  "version": "1.0.1",
@@ -48,12 +48,12 @@ describe 'AbstractControllerBase' do
48
48
 
49
49
  expect(appmap).to match(<<-CREATE_CALL.strip)
50
50
  event: call
51
+ thread_id: .*
51
52
  defined_class: Api::UsersController
52
53
  method_id: build_user
53
54
  path: app/controllers/api/users_controller.rb
54
55
  lineno: 23
55
56
  static: false
56
- thread_id: .*
57
57
  parameters:
58
58
  - name: params
59
59
  class: Hash
@@ -47,18 +47,18 @@ describe 'AbstractControllerBase' do
47
47
  SERVER_REQUEST
48
48
  end
49
49
 
50
- it 'Properly captures method parameters in the appmap' do
50
+ it 'properly captures method parameters in the appmap' do
51
51
  expect(File).to exist(appmap_json)
52
52
  appmap = JSON.parse(File.read(appmap_json)).to_yaml
53
53
 
54
54
  expect(appmap).to match(<<-CREATE_CALL.strip)
55
55
  event: call
56
+ thread_id: .*
56
57
  defined_class: Api::UsersController
57
58
  method_id: build_user
58
59
  path: app/controllers/api/users_controller.rb
59
60
  lineno: 23
60
61
  static: false
61
- thread_id: .*
62
62
  parameters:
63
63
  - name: params
64
64
  class: ActiveSupport::HashWithIndifferentAccess
@@ -68,5 +68,12 @@ describe 'AbstractControllerBase' do
68
68
  receiver:
69
69
  CREATE_CALL
70
70
  end
71
+
72
+ it 'returns a minimal event' do
73
+ expect(File).to exist(appmap_json)
74
+ appmap = JSON.parse(File.read(appmap_json))
75
+ event = appmap['events'].find { |event| event['event'] == 'return' && event['return_value'] }
76
+ expect(event.keys).to eq(%w[id event thread_id parent_id elapsed return_value])
77
+ end
71
78
  end
72
79
  end