appmap 0.25.0 → 0.28.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/CHANGELOG.md +33 -0
  4. data/README.md +123 -39
  5. data/exe/appmap +3 -57
  6. data/lib/appmap.rb +51 -32
  7. data/lib/appmap/command/record.rb +2 -61
  8. data/lib/appmap/cucumber.rb +89 -0
  9. data/lib/appmap/event.rb +1 -1
  10. data/lib/appmap/hook.rb +7 -1
  11. data/lib/appmap/metadata.rb +62 -0
  12. data/lib/appmap/middleware/remote_recording.rb +2 -7
  13. data/lib/appmap/rails/sql_handler.rb +0 -5
  14. data/lib/appmap/railtie.rb +2 -2
  15. data/lib/appmap/rspec.rb +20 -38
  16. data/lib/appmap/trace.rb +9 -9
  17. data/lib/appmap/util.rb +40 -0
  18. data/lib/appmap/version.rb +1 -1
  19. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  20. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  21. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  22. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  23. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  24. data/spec/hook_spec.rb +21 -3
  25. data/spec/rails_spec_helper.rb +2 -0
  26. data/spec/rspec_feature_metadata_spec.rb +2 -0
  27. data/spec/spec_helper.rb +8 -0
  28. data/spec/util_spec.rb +21 -0
  29. data/test/cli_test.rb +0 -13
  30. data/test/cucumber_test.rb +72 -0
  31. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  32. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  33. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  34. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  35. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  36. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  37. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  38. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  39. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  40. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  41. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  42. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  43. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  44. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  45. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  46. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  47. data/test/rspec_test.rb +5 -0
  48. metadata +26 -3
  49. data/lib/appmap/command/upload.rb +0 -101
@@ -5,66 +5,7 @@ module AppMap
5
5
  RecordStruct = Struct.new(:config, :program)
6
6
 
7
7
  class Record < RecordStruct
8
- class << self
9
- # Builds a Hash of metadata which can be detected by inspecting the system.
10
- def detect_metadata
11
- {
12
- language: {
13
- name: 'ruby',
14
- engine: RUBY_ENGINE,
15
- version: RUBY_VERSION
16
- },
17
- client: {
18
- name: 'appmap',
19
- url: AppMap::URL,
20
- version: AppMap::VERSION
21
- }
22
- }.tap do |m|
23
- if defined?(::Rails)
24
- m[:frameworks] ||= []
25
- m[:frameworks] << {
26
- name: 'rails',
27
- version: ::Rails.version
28
- }
29
- end
30
- m[:git] = git_metadata if git_available
31
- end
32
- end
33
-
34
- protected
35
-
36
- def git_available
37
- @git_available = system('git status 2>&1 > /dev/null') if @git_available.nil?
38
- end
39
-
40
- def git_metadata
41
- git_repo = `git config --get remote.origin.url`.strip
42
- git_branch = `git rev-parse --abbrev-ref HEAD`.strip
43
- git_sha = `git rev-parse HEAD`.strip
44
- git_status = `git status -s`.split("\n").map(&:strip)
45
- git_last_annotated_tag = `git describe --abbrev=0 2>/dev/null`.strip
46
- git_last_annotated_tag = nil if git_last_annotated_tag.blank?
47
- git_last_tag = `git describe --abbrev=0 --tags 2>/dev/null`.strip
48
- git_last_tag = nil if git_last_tag.blank?
49
- git_commits_since_last_annotated_tag = `git describe`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_annotated_tag
50
- git_commits_since_last_tag = `git describe --tags`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_tag
51
-
52
- {
53
- repository: git_repo,
54
- branch: git_branch,
55
- commit: git_sha,
56
- status: git_status,
57
- git_last_annotated_tag: git_last_annotated_tag,
58
- git_last_tag: git_last_tag,
59
- git_commits_since_last_annotated_tag: git_commits_since_last_annotated_tag,
60
- git_commits_since_last_tag: git_commits_since_last_tag
61
- }
62
- end
63
- end
64
-
65
8
  def perform(&block)
66
- AppMap::Hook.hook(config)
67
-
68
9
  tracer = AppMap.tracing.trace
69
10
 
70
11
  events = []
@@ -85,8 +26,8 @@ module AppMap
85
26
  quit = true
86
27
  event_thread.join
87
28
  yield AppMap::APPMAP_FORMAT_VERSION,
88
- self.class.detect_metadata,
89
- AppMap.class_map(config, tracer.event_methods),
29
+ AppMap.detect_metadata,
30
+ AppMap.class_map(tracer.event_methods),
90
31
  events
91
32
  end
92
33
 
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/util'
4
+
5
+ module AppMap
6
+ module Cucumber
7
+ ScenarioAttributes = Struct.new(:name, :feature, :feature_group)
8
+
9
+ ProviderStruct = Struct.new(:scenario) do
10
+ def feature_group
11
+ # e.g. <Cucumber::Core::Ast::Location::Precise: cucumber/api/features/authenticate.feature:1>
12
+ feature_path.split('/').last.split('.')[0]
13
+ end
14
+ end
15
+
16
+ # ProviderBefore4 provides scenario name, feature name, and feature group name for Cucumber
17
+ # versions before 4.0.
18
+ class ProviderBefore4 < ProviderStruct
19
+ def attributes
20
+ ScenarioAttributes.new(scenario.name, scenario.feature.name, feature_group)
21
+ end
22
+
23
+ def feature_path
24
+ scenario.feature.location.to_s
25
+ end
26
+ end
27
+
28
+ # Provider4 provides scenario name, feature name, and feature group name for Cucumber
29
+ # versions 4.0 and later.
30
+ class Provider4 < ProviderStruct
31
+ def attributes
32
+ ScenarioAttributes.new(scenario.name, scenario.name.split(' ')[0..1].join(' '), feature_group)
33
+ end
34
+
35
+ def feature_path
36
+ scenario.location.file
37
+ end
38
+ end
39
+
40
+ class << self
41
+ def write_scenario(scenario, appmap)
42
+ appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
43
+ scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
44
+
45
+ FileUtils.mkdir_p 'tmp/appmap/cucumber'
46
+ File.write(File.join('tmp/appmap/cucumber', scenario_filename), JSON.generate(appmap))
47
+ end
48
+
49
+ def enabled?
50
+ ENV['APPMAP'] == 'true'
51
+ end
52
+
53
+ protected
54
+
55
+ def cucumber_version
56
+ Gem.loaded_specs['cucumber']&.version&.to_s
57
+ end
58
+
59
+ def provider(scenario)
60
+ major, = cucumber_version.split('.').map(&:to_i)
61
+ if major < 4
62
+ ProviderBefore4
63
+ else
64
+ Provider4
65
+ end.new(scenario)
66
+ end
67
+
68
+ def update_metadata(scenario, base_metadata)
69
+ attributes = provider(scenario).attributes
70
+
71
+ base_metadata.tap do |m|
72
+ m['name'] = attributes.name
73
+ m['feature'] = attributes.feature
74
+ m['feature_group'] = attributes.feature_group
75
+ m['labels'] ||= []
76
+ m['labels'] += (scenario.tags&.map(&:name) || [])
77
+ m['frameworks'] ||= []
78
+ m['frameworks'] << {
79
+ 'name' => 'cucumber',
80
+ 'version' => Gem.loaded_specs['cucumber']&.version&.to_s
81
+ }
82
+ m['recorder'] = {
83
+ 'name' => 'cucumber'
84
+ }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -139,7 +139,7 @@ module AppMap
139
139
  next_exception = exception
140
140
  exceptions = []
141
141
  while next_exception
142
- exception_backtrace = next_exception.backtrace_locations[0]
142
+ exception_backtrace = next_exception.backtrace_locations.try(:[], 0)
143
143
  exceptions << {
144
144
  class: next_exception.class.name,
145
145
  message: next_exception.message,
@@ -115,7 +115,12 @@ module AppMap
115
115
  [ method.owner.name, '#' ]
116
116
  end
117
117
 
118
- warn "AppMap: Hooking #{defined_class}#{method_symbol}#{method.name}" if LOG
118
+ method_display_name = "#{defined_class}#{method_symbol}#{method.name}"
119
+ # Don't try and trace the tracing method or there will be a stack overflow
120
+ # in the defined hook method.
121
+ next if method_display_name == "AppMap.tracing"
122
+
123
+ warn "AppMap: Hooking #{method_display_name}" if LOG
119
124
 
120
125
  cls.define_method method_id do |*args, &block|
121
126
  base_method = method.bind(self).to_proc
@@ -133,6 +138,7 @@ module AppMap
133
138
  return_value = base_method.call(*args, &block)
134
139
  rescue
135
140
  exception = $ERROR_INFO
141
+ raise
136
142
  ensure
137
143
  with_disabled_hook.call do
138
144
  after_hook.call(call_event, defined_class, method, start_time, return_value, exception)
@@ -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)
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,13 +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
- require 'appmap/command/upload'
10
8
  require 'json'
11
9
 
12
10
  @app = app
13
- @config = AppMap.configure
14
- AppMap::Hook.hook(@config)
15
11
  end
16
12
 
17
13
  def event_loop
@@ -64,15 +60,14 @@ module AppMap
64
60
  @events.delete_if(&is_control_command_event)
65
61
  @events.delete_if(&is_return_from_control_command_event)
66
62
 
67
- require 'appmap/command/record'
68
- metadata = AppMap::Command::Record.detect_metadata
63
+ metadata = AppMap.detect_metadata
69
64
  metadata[:recorder] = {
70
65
  name: 'remote_recording'
71
66
  }
72
67
 
73
68
  response = JSON.generate \
74
69
  version: AppMap::APPMAP_FORMAT_VERSION,
75
- classMap: AppMap.class_map(@config, tracer.event_methods),
70
+ classMap: AppMap.class_map(tracer.event_methods),
76
71
  metadata: metadata,
77
72
  events: @events
78
73
 
@@ -102,9 +102,6 @@ module AppMap
102
102
  Thread.current[reentry_key] = true
103
103
  begin
104
104
  sql = payload[:sql].strip
105
- sql_upper = sql.upcase
106
-
107
- return unless WHITELIST.find { |keyword| sql_upper.index(keyword) == 0 }
108
105
 
109
106
  # Detect whether a function call within a specified filename is present in the call stack.
110
107
  find_in_backtrace = lambda do |file_name, function_name = nil|
@@ -143,8 +140,6 @@ module AppMap
143
140
  Thread.current[reentry_key] = nil
144
141
  end
145
142
  end
146
-
147
- WHITELIST = %w[SELECT INSERT UPDATE DELETE].freeze
148
143
  end
149
144
  end
150
145
  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
@@ -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 = 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
@@ -4,36 +4,36 @@ module AppMap
4
4
  module Trace
5
5
  ScopedMethod = Struct.new(:defined_class, :method)
6
6
 
7
- class Tracers
7
+ class Tracing
8
8
  def initialize
9
- @tracers = []
9
+ @Tracing = []
10
10
  end
11
11
 
12
12
  def empty?
13
- @tracers.empty?
13
+ @Tracing.empty?
14
14
  end
15
15
 
16
16
  def trace(enable: true)
17
17
  Tracer.new.tap do |tracer|
18
- @tracers << tracer
18
+ @Tracing << tracer
19
19
  tracer.enable if enable
20
20
  end
21
- end
21
+ end
22
22
 
23
23
  def enabled?
24
- @tracers.any?(&:enabled?)
24
+ @Tracing.any?(&:enabled?)
25
25
  end
26
26
 
27
27
  def record_event(event, defined_class: nil, method: nil)
28
- @tracers.each do |tracer|
28
+ @Tracing.each do |tracer|
29
29
  tracer.record_event(event, defined_class: defined_class, method: method)
30
30
  end
31
31
  end
32
32
 
33
33
  def delete(tracer)
34
- return unless @tracers.member?(tracer)
34
+ return unless @Tracing.member?(tracer)
35
35
 
36
- @tracers.delete(tracer)
36
+ @Tracing.delete(tracer)
37
37
  tracer.disable
38
38
  end
39
39
  end