appmap 0.27.0 → 0.28.0

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