appmap 0.27.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -3
  3. data/CHANGELOG.md +9 -0
  4. data/README.md +120 -31
  5. data/exe/appmap +3 -1
  6. data/lib/appmap.rb +40 -35
  7. data/lib/appmap/command/record.rb +2 -61
  8. data/lib/appmap/cucumber.rb +89 -0
  9. data/lib/appmap/hook.rb +6 -1
  10. data/lib/appmap/metadata.rb +62 -0
  11. data/lib/appmap/middleware/remote_recording.rb +2 -6
  12. data/lib/appmap/railtie.rb +2 -2
  13. data/lib/appmap/rspec.rb +8 -36
  14. data/lib/appmap/trace.rb +8 -8
  15. data/lib/appmap/util.rb +40 -0
  16. data/lib/appmap/version.rb +1 -1
  17. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  18. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  19. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  20. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  21. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  22. data/spec/hook_spec.rb +8 -2
  23. data/spec/rails_spec_helper.rb +2 -0
  24. data/spec/rspec_feature_metadata_spec.rb +2 -0
  25. data/spec/spec_helper.rb +4 -0
  26. data/spec/util_spec.rb +21 -0
  27. data/test/cucumber_test.rb +72 -0
  28. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  29. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  30. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  31. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  32. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  33. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  34. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  35. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  36. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  37. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  38. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  39. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  40. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  41. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  42. metadata +26 -2
@@ -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
@@ -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
@@ -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,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
 
@@ -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.deep_dup
13
+ AppMap.detect_metadata
15
14
  end
16
15
 
17
16
  module FeatureAnnotations
@@ -139,7 +138,7 @@ module AppMap
139
138
 
140
139
  AppMap::RSpec.add_event_methods @trace.event_methods
141
140
 
142
- class_map = AppMap.class_map(AppMap::RSpec.config, @trace.event_methods)
141
+ class_map = AppMap.class_map(@trace.event_methods)
143
142
 
144
143
  description = []
145
144
  scope = ScopeExample.new(example)
@@ -190,7 +189,6 @@ module AppMap
190
189
  end
191
190
 
192
191
  @recordings_by_example = {}
193
- @config = nil
194
192
  @event_methods = Set.new
195
193
 
196
194
  class << self
@@ -198,10 +196,6 @@ module AppMap
198
196
  warn 'Configuring AppMap recorder for RSpec'
199
197
 
200
198
  FileUtils.mkdir_p APPMAP_OUTPUT_DIR
201
-
202
- require 'appmap/hook'
203
- @config = AppMap.configure
204
- AppMap::Hook.hook(@config)
205
199
  end
206
200
 
207
201
  def begin_spec(example)
@@ -226,7 +220,7 @@ module AppMap
226
220
  def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
227
221
  metadata = RSpec.metadata.tap do |m|
228
222
  m[:name] = example_name
229
- m[:app] = @config.name
223
+ m[:app] = AppMap.configuration.name
230
224
  m[:feature] = feature_name if feature_name
231
225
  m[:feature_group] = feature_group_name if feature_group_name
232
226
  m[:labels] = labels if labels
@@ -246,13 +240,13 @@ module AppMap
246
240
  classMap: class_map,
247
241
  events: events
248
242
  }.compact
249
- fname = sanitize_filename(example_name)
243
+ fname = AppMap::Util.scenario_filename(example_name)
250
244
 
251
- 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))
252
246
  end
253
247
 
254
248
  def print_inventory
255
- class_map = AppMap.class_map(@config, @event_methods)
249
+ class_map = AppMap.class_map(@event_methods)
256
250
  save 'Inventory', class_map, labels: %w[inventory]
257
251
  end
258
252
 
@@ -266,28 +260,6 @@ module AppMap
266
260
  print_inventory
267
261
  end
268
262
  end
269
-
270
- private
271
-
272
- # Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
273
- # https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
274
- def sanitize_filename(fname, separator: '_')
275
- # Replace accented chars with their ASCII equivalents.
276
- fname = fname.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
277
-
278
- # Turn unwanted chars into the separator.
279
- fname.gsub!(/[^a-z0-9\-_]+/i, separator)
280
-
281
- re_sep = Regexp.escape(separator)
282
- re_duplicate_separator = /#{re_sep}{2,}/
283
- re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
284
-
285
- # No more than one of the separator in a row.
286
- fname.gsub!(re_duplicate_separator, separator)
287
-
288
- # Finally, Remove leading/trailing separator.
289
- fname.gsub(re_leading_trailing_separator, '')
290
- end
291
263
  end
292
264
  end
293
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
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
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ module Util
5
+ class << self
6
+ # scenario_filename builds a suitable file name from a scenario name.
7
+ # Special characters are removed, and the file name is truncated to fit within
8
+ # shell limitations.
9
+ def scenario_filename(name, max_length: 255, separator: '_', extension: '.appmap.json')
10
+ # Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
11
+ # https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
12
+ # Replace accented chars with their ASCII equivalents.
13
+
14
+ fname = name.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
15
+
16
+ # Turn unwanted chars into the separator.
17
+ fname.gsub!(/[^a-z0-9\-_]+/i, separator)
18
+
19
+ re_sep = Regexp.escape(separator)
20
+ re_duplicate_separator = /#{re_sep}{2,}/
21
+ re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
22
+
23
+ # No more than one of the separator in a row.
24
+ fname.gsub!(re_duplicate_separator, separator)
25
+
26
+ # Finally, Remove leading/trailing separator.
27
+ fname.gsub!(re_leading_trailing_separator, '')
28
+
29
+ if (fname.length + extension.length) > max_length
30
+ require 'base64'
31
+ require 'digest'
32
+ fname_digest = Base64.urlsafe_encode64 Digest::MD5.digest(fname), padding: false
33
+ fname[max_length - fname_digest.length - extension.length - 1..-1] = [ '-', fname_digest ].join
34
+ end
35
+
36
+ [ fname, extension ].join
37
+ end
38
+ end
39
+ end
40
+ end
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.27.0'
6
+ VERSION = '0.28.0'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.2'
9
9
  end
@@ -39,6 +39,7 @@ appmap_options = \
39
39
  gem 'appmap', appmap_options
40
40
 
41
41
  group :development, :test do
42
+ gem 'cucumber-rails', require: false
42
43
  gem 'rspec-rails'
43
44
  # Required for Sequel, since without ActiveRecord, the Rails transactional fixture support
44
45
  # isn't activated.
@@ -0,0 +1,13 @@
1
+ Feature: /api/users
2
+
3
+ @appmap-disable
4
+ Scenario: A user can be created
5
+ When I create a user
6
+ Then the response status should be 201
7
+
8
+ Scenario: When a user is created, it should be in the user list
9
+ Given I create a user
10
+ And the response status should be 201
11
+ When I list the users
12
+ Then the response status should be 200
13
+ And the response should include the user
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cucumber/rails'
4
+ require 'appmap/cucumber'
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ if AppMap::Cucumber.enabled?
4
+ Around('not @appmap-disable') do |scenario, block|
5
+ appmap = AppMap.record do
6
+ block.call
7
+ end
8
+
9
+ AppMap::Cucumber.write_scenario(scenario, appmap)
10
+ end
11
+ end