appmap 0.26.0 → 0.31.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +144 -31
  5. data/Rakefile +1 -1
  6. data/exe/appmap +3 -1
  7. data/lib/appmap.rb +55 -35
  8. data/lib/appmap/algorithm/stats.rb +2 -1
  9. data/lib/appmap/class_map.rb +16 -24
  10. data/lib/appmap/command/record.rb +2 -61
  11. data/lib/appmap/config.rb +91 -0
  12. data/lib/appmap/cucumber.rb +89 -0
  13. data/lib/appmap/event.rb +6 -6
  14. data/lib/appmap/hook.rb +94 -116
  15. data/lib/appmap/metadata.rb +62 -0
  16. data/lib/appmap/middleware/remote_recording.rb +2 -6
  17. data/lib/appmap/minitest.rb +141 -0
  18. data/lib/appmap/rails/action_handler.rb +2 -2
  19. data/lib/appmap/rails/sql_handler.rb +2 -2
  20. data/lib/appmap/railtie.rb +2 -2
  21. data/lib/appmap/record.rb +27 -0
  22. data/lib/appmap/rspec.rb +20 -38
  23. data/lib/appmap/trace.rb +19 -11
  24. data/lib/appmap/util.rb +40 -0
  25. data/lib/appmap/version.rb +1 -1
  26. data/package-lock.json +3 -3
  27. data/spec/abstract_controller4_base_spec.rb +1 -1
  28. data/spec/abstract_controller_base_spec.rb +1 -1
  29. data/spec/config_spec.rb +3 -3
  30. data/spec/fixtures/hook/compare.rb +7 -0
  31. data/spec/fixtures/hook/openssl_sign.rb +87 -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 +243 -36
  39. data/spec/rails_spec_helper.rb +2 -0
  40. data/spec/rspec_feature_metadata_spec.rb +2 -0
  41. data/spec/spec_helper.rb +4 -0
  42. data/spec/util_spec.rb +21 -0
  43. data/test/cli_test.rb +2 -2
  44. data/test/cucumber_test.rb +72 -0
  45. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  46. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  47. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  48. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  49. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  50. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  51. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  52. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  53. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  54. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  55. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  56. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  57. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  58. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  59. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  60. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  61. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  62. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  63. data/test/fixtures/process_recorder/appmap.yml +3 -0
  64. data/test/fixtures/process_recorder/hello.rb +9 -0
  65. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  66. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  67. data/test/minitest_test.rb +38 -0
  68. data/test/record_process_test.rb +35 -0
  69. data/test/rspec_test.rb +5 -0
  70. metadata +39 -3
  71. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ module Metadata
5
+ class << self
6
+ def detect
7
+ {
8
+ app: AppMap.configuration.name,
9
+ language: {
10
+ name: 'ruby',
11
+ engine: RUBY_ENGINE,
12
+ version: RUBY_VERSION
13
+ },
14
+ client: {
15
+ name: 'appmap',
16
+ url: AppMap::URL,
17
+ version: AppMap::VERSION
18
+ }
19
+ }.tap do |m|
20
+ if defined?(::Rails) && defined?(::Rails.version)
21
+ m[:frameworks] ||= []
22
+ m[:frameworks] << {
23
+ name: 'rails',
24
+ version: ::Rails.version
25
+ }
26
+ end
27
+ m[:git] = git_metadata if git_available
28
+ end
29
+ end
30
+
31
+ protected
32
+
33
+ def git_available
34
+ @git_available = system('git status 2>&1 > /dev/null') if @git_available.nil?
35
+ end
36
+
37
+ def git_metadata
38
+ git_repo = `git config --get remote.origin.url`.strip
39
+ git_branch = `git rev-parse --abbrev-ref HEAD`.strip
40
+ git_sha = `git rev-parse HEAD`.strip
41
+ git_status = `git status -s`.split("\n").map(&:strip)
42
+ git_last_annotated_tag = `git describe --abbrev=0 2>/dev/null`.strip
43
+ git_last_annotated_tag = nil if git_last_annotated_tag.blank?
44
+ git_last_tag = `git describe --abbrev=0 --tags 2>/dev/null`.strip
45
+ git_last_tag = nil if git_last_tag.blank?
46
+ git_commits_since_last_annotated_tag = `git describe`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_annotated_tag
47
+ git_commits_since_last_tag = `git describe --tags`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_tag
48
+
49
+ {
50
+ repository: git_repo,
51
+ branch: git_branch,
52
+ commit: git_sha,
53
+ status: git_status,
54
+ git_last_annotated_tag: git_last_annotated_tag,
55
+ git_last_tag: git_last_tag,
56
+ git_commits_since_last_annotated_tag: git_commits_since_last_annotated_tag,
57
+ git_commits_since_last_tag: git_commits_since_last_tag
58
+ }
59
+ end
60
+ end
61
+ end
62
+ end
@@ -5,12 +5,9 @@ module AppMap
5
5
  # RemoteRecording adds `/_appmap/record` routes to control recordings via HTTP requests
6
6
  class RemoteRecording
7
7
  def initialize(app)
8
- require 'appmap/command/record'
9
8
  require 'json'
10
9
 
11
10
  @app = app
12
- @config = AppMap.configure
13
- AppMap::Hook.hook(@config)
14
11
  end
15
12
 
16
13
  def event_loop
@@ -63,15 +60,14 @@ module AppMap
63
60
  @events.delete_if(&is_control_command_event)
64
61
  @events.delete_if(&is_return_from_control_command_event)
65
62
 
66
- require 'appmap/command/record'
67
- metadata = AppMap::Command::Record.detect_metadata
63
+ metadata = AppMap.detect_metadata
68
64
  metadata[:recorder] = {
69
65
  name: 'remote_recording'
70
66
  }
71
67
 
72
68
  response = JSON.generate \
73
69
  version: AppMap::APPMAP_FORMAT_VERSION,
74
- classMap: AppMap.class_map(@config, tracer.event_methods),
70
+ classMap: AppMap.class_map(tracer.event_methods),
75
71
  metadata: metadata,
76
72
  events: @events
77
73
 
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/util'
4
+
5
+ module AppMap
6
+ # Integration of AppMap with Minitest. When enabled with APPMAP=true, the AppMap tracer will
7
+ # be activated around each test.
8
+ module Minitest
9
+ APPMAP_OUTPUT_DIR = 'tmp/appmap/minitest'
10
+ LOG = false
11
+
12
+ def self.metadata
13
+ AppMap.detect_metadata
14
+ end
15
+
16
+ Recording = Struct.new(:test) do
17
+ def initialize(test)
18
+ super
19
+
20
+ warn "Starting recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
21
+ @trace = AppMap.tracing.trace
22
+ end
23
+
24
+ def finish
25
+ warn "Finishing recording of test #{test.class}.#{test.name}" if AppMap::Minitest::LOG
26
+
27
+ events = []
28
+ AppMap.tracing.delete @trace
29
+
30
+ events << @trace.next_event.to_h while @trace.event?
31
+
32
+ AppMap::Minitest.add_event_methods @trace.event_methods
33
+
34
+ class_map = AppMap.class_map(@trace.event_methods)
35
+
36
+ feature_group = test.class.name.underscore.split('_')[0...-1].join('_').capitalize
37
+ feature_name = test.name.split('_')[1..-1].join(' ')
38
+ scenario_name = [ feature_group, feature_name ].join(' ')
39
+
40
+ AppMap::Minitest.save scenario_name,
41
+ class_map,
42
+ events: events,
43
+ feature_name: feature_name,
44
+ feature_group_name: feature_group
45
+ end
46
+ end
47
+
48
+ @recordings_by_test = {}
49
+ @event_methods = Set.new
50
+
51
+ class << self
52
+ def init
53
+ warn 'Configuring AppMap recorder for Minitest'
54
+
55
+ FileUtils.mkdir_p APPMAP_OUTPUT_DIR
56
+ end
57
+
58
+ def begin_test(test)
59
+ @recordings_by_test[test.object_id] = Recording.new(test)
60
+ end
61
+
62
+ def end_test(test)
63
+ recording = @recordings_by_test.delete(test.object_id)
64
+ return warn "No recording found for #{test}" unless recording
65
+
66
+ recording.finish
67
+ end
68
+
69
+ def config
70
+ @config or raise "AppMap is not configured"
71
+ end
72
+
73
+ def add_event_methods(event_methods)
74
+ @event_methods += event_methods
75
+ end
76
+
77
+ def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
78
+ metadata = AppMap::Minitest.metadata.tap do |m|
79
+ m[:name] = example_name
80
+ m[:app] = AppMap.configuration.name
81
+ m[:feature] = feature_name if feature_name
82
+ m[:feature_group] = feature_group_name if feature_group_name
83
+ m[:frameworks] ||= []
84
+ m[:frameworks] << {
85
+ name: 'minitest',
86
+ version: Gem.loaded_specs['minitest']&.version&.to_s
87
+ }
88
+ m[:recorder] = {
89
+ name: 'minitest'
90
+ }
91
+ end
92
+
93
+ appmap = {
94
+ version: AppMap::APPMAP_FORMAT_VERSION,
95
+ metadata: metadata,
96
+ classMap: class_map,
97
+ events: events
98
+ }.compact
99
+ fname = AppMap::Util.scenario_filename(example_name)
100
+
101
+ File.write(File.join(APPMAP_OUTPUT_DIR, fname), JSON.generate(appmap))
102
+ end
103
+
104
+ def print_inventory
105
+ class_map = AppMap.class_map(@event_methods)
106
+ save 'Inventory', class_map, labels: %w[inventory]
107
+ end
108
+
109
+ def enabled?
110
+ ENV['APPMAP'] == 'true'
111
+ end
112
+
113
+ def run
114
+ init
115
+ at_exit do
116
+ print_inventory
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
122
+
123
+ if AppMap::Minitest.enabled?
124
+ require 'appmap'
125
+ require 'minitest/test'
126
+
127
+ class ::Minitest::Test
128
+ alias run_without_hook run
129
+
130
+ def run
131
+ AppMap::Minitest.begin_test self
132
+ begin
133
+ run_without_hook
134
+ ensure
135
+ AppMap::Minitest.end_test self
136
+ end
137
+ end
138
+ end
139
+
140
+ AppMap::Minitest.run
141
+ end
@@ -20,7 +20,7 @@ module AppMap
20
20
  attr_accessor :payload
21
21
 
22
22
  def initialize(path, lineno, payload)
23
- super AppMap::Event.next_id_counter, :call, HTTPServerRequest, :call, path, lineno, false, Thread.current.object_id
23
+ super AppMap::Event.next_id_counter, :call, HTTPServerRequest, :call, path, lineno, Thread.current.object_id
24
24
 
25
25
  self.payload = payload
26
26
  end
@@ -60,7 +60,7 @@ module AppMap
60
60
  attr_accessor :payload
61
61
 
62
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
63
+ super AppMap::Event.next_id_counter, :return, HTTPServerResponse, :call, path, lineno, Thread.current.object_id
64
64
 
65
65
  self.payload = payload
66
66
  self.parent_id = parent_id
@@ -9,7 +9,7 @@ module AppMap
9
9
  attr_accessor :payload
10
10
 
11
11
  def initialize(path, lineno, payload)
12
- super AppMap::Event.next_id_counter, :call, SQLHandler, :call, path, lineno, false, Thread.current.object_id
12
+ super AppMap::Event.next_id_counter, :call, SQLHandler, :call, path, lineno, Thread.current.object_id
13
13
 
14
14
  self.payload = payload
15
15
  end
@@ -30,7 +30,7 @@ module AppMap
30
30
 
31
31
  class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
32
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
33
+ super AppMap::Event.next_id_counter, :return, SQLHandler, :call, path, lineno, Thread.current.object_id
34
34
 
35
35
  self.parent_id = parent_id
36
36
  self.elapsed = elapsed
@@ -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.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.dup.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.