appmap 0.28.0 → 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +54 -2
  4. data/Rakefile +1 -1
  5. data/appmap.gemspec +1 -0
  6. data/lib/appmap.rb +25 -14
  7. data/lib/appmap/algorithm/stats.rb +2 -1
  8. data/lib/appmap/class_map.rb +26 -28
  9. data/lib/appmap/config.rb +115 -0
  10. data/lib/appmap/event.rb +28 -19
  11. data/lib/appmap/hook.rb +88 -129
  12. data/lib/appmap/hook/method.rb +78 -0
  13. data/lib/appmap/metadata.rb +1 -1
  14. data/lib/appmap/minitest.rb +141 -0
  15. data/lib/appmap/open.rb +57 -0
  16. data/lib/appmap/rails/action_handler.rb +7 -7
  17. data/lib/appmap/rails/sql_handler.rb +10 -8
  18. data/lib/appmap/record.rb +27 -0
  19. data/lib/appmap/rspec.rb +2 -2
  20. data/lib/appmap/trace.rb +17 -9
  21. data/lib/appmap/util.rb +19 -0
  22. data/lib/appmap/version.rb +1 -1
  23. data/package-lock.json +3 -3
  24. data/spec/abstract_controller4_base_spec.rb +1 -1
  25. data/spec/abstract_controller_base_spec.rb +9 -2
  26. data/spec/config_spec.rb +3 -3
  27. data/spec/fixtures/hook/compare.rb +7 -0
  28. data/spec/fixtures/hook/singleton_method.rb +54 -0
  29. data/spec/hook_spec.rb +280 -53
  30. data/spec/open_spec.rb +19 -0
  31. data/spec/record_sql_rails_pg_spec.rb +56 -33
  32. data/spec/util_spec.rb +1 -1
  33. data/test/cli_test.rb +14 -4
  34. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  35. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  36. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  37. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  38. data/test/fixtures/openssl_recorder/Gemfile +3 -0
  39. data/test/fixtures/openssl_recorder/appmap.yml +3 -0
  40. data/test/fixtures/openssl_recorder/lib/openssl_cert_sign.rb +94 -0
  41. data/test/fixtures/openssl_recorder/lib/openssl_encrypt.rb +34 -0
  42. data/test/fixtures/openssl_recorder/lib/openssl_key_sign.rb +28 -0
  43. data/test/fixtures/process_recorder/appmap.yml +3 -0
  44. data/test/fixtures/process_recorder/hello.rb +9 -0
  45. data/test/minitest_test.rb +38 -0
  46. data/test/openssl_test.rb +203 -0
  47. data/test/record_process_test.rb +35 -0
  48. data/test/test_helper.rb +1 -0
  49. metadata +38 -4
  50. 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
@@ -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
@@ -154,7 +154,7 @@ module AppMap
154
154
  end
155
155
 
156
156
  labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
157
- description.reject!(&:nil?).reject(&:blank?)
157
+ description.reject!(&:nil?).reject!(&:blank?)
158
158
  default_description = description.last
159
159
  description.reverse!
160
160
 
@@ -218,7 +218,7 @@ module AppMap
218
218
  end
219
219
 
220
220
  def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
221
- metadata = RSpec.metadata.tap do |m|
221
+ metadata = AppMap::RSpec.metadata.tap do |m|
222
222
  m[:name] = example_name
223
223
  m[:app] = AppMap.configuration.name
224
224
  m[:feature] = feature_name if feature_name
@@ -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
15
  class Tracing
8
16
  def initialize
9
- @Tracing = []
17
+ @tracing = []
10
18
  end
11
19
 
12
20
  def empty?
13
- @Tracing.empty?
21
+ @tracing.empty?
14
22
  end
15
23
 
16
24
  def trace(enable: true)
17
25
  Tracer.new.tap do |tracer|
18
- @Tracing << tracer
26
+ @tracing << tracer
19
27
  tracer.enable if enable
20
28
  end
21
29
  end
22
30
 
23
31
  def enabled?
24
- @Tracing.any?(&:enabled?)
32
+ @tracing.any?(&:enabled?)
25
33
  end
26
34
 
27
35
  def record_event(event, defined_class: nil, method: nil)
28
- @Tracing.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 @Tracing.member?(tracer)
42
+ return unless @tracing.member?(tracer)
35
43
 
36
- @Tracing.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.
@@ -35,6 +35,25 @@ module AppMap
35
35
 
36
36
  [ fname, extension ].join
37
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
38
57
  end
39
58
  end
40
59
  end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.28.0'
6
+ VERSION = '0.34.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
@@ -2,9 +2,9 @@
2
2
 
3
3
  require 'rails_spec_helper'
4
4
  require 'active_support/core_ext'
5
- require 'appmap/hook'
5
+ require 'appmap/config'
6
6
 
7
- describe AppMap::Hook::Config do
7
+ describe AppMap::Config, docker: false do
8
8
  it 'loads from a Hash' do
9
9
  config_data = {
10
10
  name: 'test',
@@ -18,7 +18,7 @@ describe AppMap::Hook::Config do
18
18
  }
19
19
  ]
20
20
  }.deep_stringify_keys!
21
- config = AppMap::Hook::Config.load(config_data)
21
+ config = AppMap::Config.load(config_data)
22
22
 
23
23
  expect(config.to_h.deep_stringify_keys!).to eq(config_data)
24
24
  end
@@ -0,0 +1,7 @@
1
+ require 'active_support/security_utils'
2
+
3
+ class Compare
4
+ def self.compare(s1, s2)
5
+ ActiveSupport::SecurityUtils.secure_compare(s1, s2)
6
+ end
7
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SingletonMethod
4
+ class << self
5
+ def say_default
6
+ 'default'
7
+ end
8
+ end
9
+
10
+ def SingletonMethod.say_class_defined
11
+ 'defined with explicit class scope'
12
+ end
13
+
14
+ def self.say_self_defined
15
+ 'defined with self class scope'
16
+ end
17
+
18
+ # When called, do_include calls +include+ to bring in the module
19
+ # AddMethod. AddMethod defines a new instance method, which gets
20
+ # added to the singleton class of SingletonMethod.
21
+ def do_include
22
+ class << self
23
+ SingletonMethod.include(AddMethod)
24
+ end
25
+ self
26
+ end
27
+
28
+ def self.new_with_instance_method
29
+ SingletonMethod.new.tap do |m|
30
+ def m.say_instance_defined
31
+ 'defined for an instance'
32
+ end
33
+ end
34
+ end
35
+
36
+ def to_s
37
+ 'Singleton Method fixture'
38
+ end
39
+ end
40
+
41
+ module AddMethod
42
+ def self.included(base)
43
+ base.module_eval do
44
+ define_method "added_method" do
45
+ _added_method
46
+ end
47
+ end
48
+ end
49
+
50
+ def _added_method
51
+ 'defined by including a module'
52
+ end
53
+ end
54
+