appmap 0.26.0 → 0.31.0

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