appmap 0.27.0 → 0.33.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 (73) 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/appmap.gemspec +1 -0
  7. data/exe/appmap +3 -1
  8. data/lib/appmap.rb +54 -38
  9. data/lib/appmap/algorithm/stats.rb +2 -1
  10. data/lib/appmap/class_map.rb +21 -28
  11. data/lib/appmap/command/record.rb +2 -61
  12. data/lib/appmap/config.rb +89 -0
  13. data/lib/appmap/cucumber.rb +89 -0
  14. data/lib/appmap/event.rb +28 -19
  15. data/lib/appmap/hook.rb +56 -128
  16. data/lib/appmap/hook/method.rb +78 -0
  17. data/lib/appmap/metadata.rb +62 -0
  18. data/lib/appmap/middleware/remote_recording.rb +2 -6
  19. data/lib/appmap/minitest.rb +141 -0
  20. data/lib/appmap/open.rb +57 -0
  21. data/lib/appmap/rails/action_handler.rb +7 -7
  22. data/lib/appmap/rails/sql_handler.rb +10 -8
  23. data/lib/appmap/railtie.rb +2 -2
  24. data/lib/appmap/record.rb +27 -0
  25. data/lib/appmap/rspec.rb +9 -37
  26. data/lib/appmap/trace.rb +18 -10
  27. data/lib/appmap/util.rb +59 -0
  28. data/lib/appmap/version.rb +1 -1
  29. data/package-lock.json +3 -3
  30. data/spec/abstract_controller4_base_spec.rb +1 -1
  31. data/spec/abstract_controller_base_spec.rb +9 -2
  32. data/spec/config_spec.rb +3 -3
  33. data/spec/fixtures/hook/compare.rb +7 -0
  34. data/spec/fixtures/hook/singleton_method.rb +54 -0
  35. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  36. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  37. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  38. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  39. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  40. data/spec/hook_spec.rb +228 -53
  41. data/spec/open_spec.rb +19 -0
  42. data/spec/rails_spec_helper.rb +2 -0
  43. data/spec/record_sql_rails_pg_spec.rb +56 -33
  44. data/spec/rspec_feature_metadata_spec.rb +2 -0
  45. data/spec/spec_helper.rb +4 -0
  46. data/spec/util_spec.rb +21 -0
  47. data/test/cli_test.rb +4 -4
  48. data/test/cucumber_test.rb +72 -0
  49. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  50. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  51. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  52. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  53. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  54. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  55. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  56. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  57. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  58. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  59. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  60. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  61. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  62. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  63. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  64. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  65. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  66. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  67. data/test/fixtures/process_recorder/appmap.yml +3 -0
  68. data/test/fixtures/process_recorder/hello.rb +9 -0
  69. data/test/minitest_test.rb +38 -0
  70. data/test/record_process_test.rb +35 -0
  71. data/test/test_helper.rb +1 -0
  72. metadata +55 -3
  73. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ OpenStruct = Struct.new(:appmap)
5
+
6
+ class Open < OpenStruct
7
+ attr_reader :port
8
+
9
+ def perform
10
+ server = run_server
11
+ open_browser
12
+ server.kill
13
+ end
14
+
15
+ def page
16
+ require 'rack/utils'
17
+ <<~PAGE
18
+ <!DOCTYPE html>
19
+ <html>
20
+ <head>
21
+ <title>&hellip;</title>
22
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
23
+ <script type="text/javascript">
24
+ function dosubmit() { document.forms[0].submit(); }
25
+ </script>
26
+ </head>
27
+ <body onload="dosubmit();">
28
+ <form action="https://app.land/scenario_uploads" method="POST" accept-charset="utf-8">
29
+ <input type="hidden" name="data" value='#{Rack::Utils.escape_html appmap.to_json}'>
30
+ </form>
31
+ </body>
32
+ </html>
33
+ PAGE
34
+ end
35
+
36
+ def run_server
37
+ require 'rack'
38
+ Thread.new do
39
+ Rack::Handler::WEBrick.run(
40
+ lambda do |env|
41
+ return [200, { 'Content-Type' => 'text/html' }, [page]]
42
+ end,
43
+ :Port => 0
44
+ ) do |server|
45
+ @port = server.config[:Port]
46
+ end
47
+ end.tap do
48
+ sleep 1.0
49
+ end
50
+ end
51
+
52
+ def open_browser
53
+ system 'open', "http://localhost:#{@port}"
54
+ sleep 5.0
55
+ end
56
+ end
57
+ end
@@ -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
@@ -139,7 +138,7 @@ module AppMap
139
138
 
140
139
  AppMap::RSpec.add_event_methods @trace.event_methods
141
140
 
142
- class_map = AppMap.class_map(AppMap::RSpec.config, @trace.event_methods)
141
+ class_map = AppMap.class_map(@trace.event_methods)
143
142
 
144
143
  description = []
145
144
  scope = ScopeExample.new(example)
@@ -190,7 +189,6 @@ module AppMap
190
189
  end
191
190
 
192
191
  @recordings_by_example = {}
193
- @config = nil
194
192
  @event_methods = Set.new
195
193
 
196
194
  class << self
@@ -198,10 +196,6 @@ module AppMap
198
196
  warn 'Configuring AppMap recorder for RSpec'
199
197
 
200
198
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
201
-
202
- require 'appmap/hook'
203
- @config = AppMap.configure
204
- AppMap::Hook.hook(@config)
205
199
  end
206
200
 
207
201
  def begin_spec(example)
@@ -224,9 +218,9 @@ module AppMap
224
218
  end
225
219
 
226
220
  def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
227
- metadata = RSpec.metadata.tap do |m|
221
+ metadata = AppMap::RSpec.metadata.tap do |m|
228
222
  m[:name] = example_name
229
- m[:app] = @config.name
223
+ m[:app] = AppMap.configuration.name
230
224
  m[:feature] = feature_name if feature_name
231
225
  m[:feature_group] = feature_group_name if feature_group_name
232
226
  m[:labels] = labels if labels
@@ -246,13 +240,13 @@ module AppMap
246
240
  classMap: class_map,
247
241
  events: events
248
242
  }.compact
249
- fname = sanitize_filename(example_name)
243
+ fname = AppMap::Util.scenario_filename(example_name)
250
244
 
251
- 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))
252
246
  end
253
247
 
254
248
  def print_inventory
255
- class_map = AppMap.class_map(@config, @event_methods)
249
+ class_map = AppMap.class_map(@event_methods)
256
250
  save 'Inventory', class_map, labels: %w[inventory]
257
251
  end
258
252
 
@@ -266,28 +260,6 @@ module AppMap
266
260
  print_inventory
267
261
  end
268
262
  end
269
-
270
- private
271
-
272
- # Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
273
- # https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
274
- def sanitize_filename(fname, separator: '_')
275
- # Replace accented chars with their ASCII equivalents.
276
- fname = fname.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
277
-
278
- # Turn unwanted chars into the separator.
279
- fname.gsub!(/[^a-z0-9\-_]+/i, separator)
280
-
281
- re_sep = Regexp.escape(separator)
282
- re_duplicate_separator = /#{re_sep}{2,}/
283
- re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
284
-
285
- # No more than one of the separator in a row.
286
- fname.gsub!(re_duplicate_separator, separator)
287
-
288
- # Finally, Remove leading/trailing separator.
289
- fname.gsub(re_leading_trailing_separator, '')
290
- end
291
263
  end
292
264
  end
293
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
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.27.0'
6
+ VERSION = '0.33.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