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.
- checksums.yaml +4 -4
- data/.gitignore +1 -3
- data/CHANGELOG.md +9 -0
- data/README.md +120 -31
- data/exe/appmap +3 -1
- data/lib/appmap.rb +40 -35
- data/lib/appmap/command/record.rb +2 -61
- data/lib/appmap/cucumber.rb +89 -0
- data/lib/appmap/hook.rb +6 -1
- data/lib/appmap/metadata.rb +62 -0
- data/lib/appmap/middleware/remote_recording.rb +2 -6
- data/lib/appmap/railtie.rb +2 -2
- data/lib/appmap/rspec.rb +8 -36
- data/lib/appmap/trace.rb +8 -8
- data/lib/appmap/util.rb +40 -0
- data/lib/appmap/version.rb +1 -1
- data/spec/fixtures/rails_users_app/Gemfile +1 -0
- data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
- data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
- data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
- data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
- data/spec/hook_spec.rb +8 -2
- data/spec/rails_spec_helper.rb +2 -0
- data/spec/rspec_feature_metadata_spec.rb +2 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/util_spec.rb +21 -0
- data/test/cucumber_test.rb +72 -0
- data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
- data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
- data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
- data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
- data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
- data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
- data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
- data/test/fixtures/cucumber_recorder/Gemfile +5 -0
- data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
- data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
- data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
- data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
- data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
- data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
- 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
|
data/lib/appmap/hook.rb
CHANGED
@@ -115,7 +115,12 @@ module AppMap
|
|
115
115
|
[ method.owner.name, '#' ]
|
116
116
|
end
|
117
117
|
|
118
|
-
|
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
|
-
|
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(
|
70
|
+
classMap: AppMap.class_map(tracer.event_methods),
|
75
71
|
metadata: metadata,
|
76
72
|
events: @events
|
77
73
|
|
data/lib/appmap/railtie.rb
CHANGED
@@ -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 |_| #
|
9
|
-
|
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
|
data/lib/appmap/rspec.rb
CHANGED
@@ -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
|
-
|
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(
|
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] =
|
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 =
|
243
|
+
fname = AppMap::Util.scenario_filename(example_name)
|
250
244
|
|
251
|
-
File.write(File.join(APPMAP_OUTPUT_DIR,
|
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(@
|
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
|
data/lib/appmap/trace.rb
CHANGED
@@ -4,36 +4,36 @@ module AppMap
|
|
4
4
|
module Trace
|
5
5
|
ScopedMethod = Struct.new(:defined_class, :method)
|
6
6
|
|
7
|
-
class
|
7
|
+
class Tracing
|
8
8
|
def initialize
|
9
|
-
@
|
9
|
+
@Tracing = []
|
10
10
|
end
|
11
11
|
|
12
12
|
def empty?
|
13
|
-
@
|
13
|
+
@Tracing.empty?
|
14
14
|
end
|
15
15
|
|
16
16
|
def trace(enable: true)
|
17
17
|
Tracer.new.tap do |tracer|
|
18
|
-
@
|
18
|
+
@Tracing << tracer
|
19
19
|
tracer.enable if enable
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
def enabled?
|
24
|
-
@
|
24
|
+
@Tracing.any?(&:enabled?)
|
25
25
|
end
|
26
26
|
|
27
27
|
def record_event(event, defined_class: nil, method: nil)
|
28
|
-
@
|
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 @
|
34
|
+
return unless @Tracing.member?(tracer)
|
35
35
|
|
36
|
-
@
|
36
|
+
@Tracing.delete(tracer)
|
37
37
|
tracer.disable
|
38
38
|
end
|
39
39
|
end
|
data/lib/appmap/util.rb
ADDED
@@ -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
|
data/lib/appmap/version.rb
CHANGED
@@ -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
|