appmap 0.27.0 → 0.33.0

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