appmap 0.25.0 → 0.28.0

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