appmap 0.23.0 → 0.25.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 -0
- data/.rubocop.yml +17 -8
- data/.travis.yml +6 -0
- data/CHANGELOG.md +19 -0
- data/README.md +29 -12
- data/Rakefile +3 -3
- data/appmap.gemspec +3 -1
- data/exe/appmap +6 -18
- data/lib/appmap.rb +47 -6
- data/lib/appmap/algorithm/prune_class_map.rb +2 -0
- data/lib/appmap/algorithm/stats.rb +4 -2
- data/lib/appmap/class_map.rb +143 -0
- data/lib/appmap/command/record.rb +8 -6
- data/lib/appmap/command/stats.rb +2 -0
- data/lib/appmap/command/upload.rb +4 -2
- data/lib/appmap/event.rb +168 -0
- data/lib/appmap/hook.rb +151 -0
- data/lib/appmap/middleware/remote_recording.rb +14 -20
- data/lib/appmap/rails/action_handler.rb +10 -6
- data/lib/appmap/rails/sql_handler.rb +10 -8
- data/lib/appmap/railtie.rb +31 -18
- data/lib/appmap/rspec.rb +238 -261
- data/lib/appmap/trace.rb +88 -0
- data/lib/appmap/version.rb +1 -1
- data/package-lock.json +90 -92
- data/spec/abstract_controller4_base_spec.rb +1 -1
- data/spec/abstract_controller_base_spec.rb +7 -3
- data/spec/config_spec.rb +25 -0
- data/spec/fixtures/hook/attr_accessor.rb +5 -0
- data/spec/fixtures/hook/class_method.rb +17 -0
- data/spec/fixtures/hook/constructor.rb +7 -0
- data/spec/fixtures/hook/exception_method.rb +11 -0
- data/spec/fixtures/hook/instance_method.rb +23 -0
- data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +3 -3
- data/spec/fixtures/rails4_users_app/config/database.yml +2 -1
- data/spec/fixtures/rails4_users_app/docker-compose.yml +2 -0
- data/spec/fixtures/rails_users_app/.ruby-version +1 -1
- data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +2 -2
- data/spec/fixtures/rails_users_app/config/database.yml +2 -1
- data/spec/fixtures/rails_users_app/create_app +1 -0
- data/spec/fixtures/rails_users_app/docker-compose.yml +4 -0
- data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +1 -1
- data/spec/hook_spec.rb +357 -0
- data/spec/rails_spec_helper.rb +25 -16
- data/spec/railtie_spec.rb +1 -1
- data/spec/record_sql_rails_pg_spec.rb +1 -2
- data/spec/remote_recording_spec.rb +117 -0
- data/spec/spec_helper.rb +1 -0
- data/test/cli_test.rb +7 -36
- data/test/fixtures/cli_record_test/appmap.yml +2 -1
- data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +4 -2
- data/test/test_helper.rb +0 -42
- metadata +46 -62
- data/exe/_appmap-record-self +0 -49
- data/lib/appmap/command/inspect.rb +0 -14
- data/lib/appmap/config.rb +0 -65
- data/lib/appmap/config/directory.rb +0 -65
- data/lib/appmap/config/file.rb +0 -13
- data/lib/appmap/config/named_function.rb +0 -21
- data/lib/appmap/config/package_dir.rb +0 -52
- data/lib/appmap/config/path.rb +0 -25
- data/lib/appmap/feature.rb +0 -262
- data/lib/appmap/inspect.rb +0 -91
- data/lib/appmap/inspect/inspector.rb +0 -99
- data/lib/appmap/inspect/parse_node.rb +0 -170
- data/lib/appmap/inspect/parser.rb +0 -15
- data/lib/appmap/parser.rb +0 -60
- data/lib/appmap/rspec/parse_node.rb +0 -41
- data/lib/appmap/rspec/parser.rb +0 -15
- data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +0 -65
- data/lib/appmap/trace/tracer.rb +0 -356
- data/spec/fixtures/rails_users_app/bin/_appmap-record-self +0 -29
- data/spec/rack_handler_webrick_spec.rb +0 -59
- data/test/config_test.rb +0 -149
- data/test/explict_inspect_test.rb +0 -29
- data/test/fixtures/active_record_like/active_record.rb +0 -2
- data/test/fixtures/active_record_like/active_record/aggregations.rb +0 -4
- data/test/fixtures/active_record_like/active_record/association.rb +0 -4
- data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +0 -6
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +0 -8
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +0 -8
- data/test/fixtures/active_record_like/active_record/caps/caps.rb +0 -4
- data/test/fixtures/ignore_non_ruby_file/class.rb +0 -3
- data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +0 -1
- data/test/fixtures/includes_excludes/lib/a/a_1.rb +0 -6
- data/test/fixtures/includes_excludes/lib/a/a_2.rb +0 -6
- data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +0 -8
- data/test/fixtures/includes_excludes/lib/b/b_1.rb +0 -6
- data/test/fixtures/includes_excludes/lib/root_1.rb +0 -4
- data/test/fixtures/inspect_multiple_subdirs/module_a.rb +0 -2
- data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +0 -5
- data/test/fixtures/inspect_multiple_subdirs/module_b.rb +0 -2
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +0 -5
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +0 -5
- data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +0 -6
- data/test/fixtures/parse_file/defs_static_function.rb +0 -96
- data/test/fixtures/parse_file/function_within_class.rb +0 -36
- data/test/fixtures/parse_file/include_public_methods.rb +0 -127
- data/test/fixtures/parse_file/instance_function.rb +0 -17
- data/test/fixtures/parse_file/modules.rb +0 -71
- data/test/fixtures/parse_file/sclass_static_function.rb +0 -88
- data/test/fixtures/parse_file/toplevel_class.rb +0 -13
- data/test/fixtures/parse_file/toplevel_function.rb +0 -14
- data/test/fixtures/trace_test/trace_program_1.rb +0 -44
- data/test/implicit_inspect_test.rb +0 -33
- data/test/include_exclude_test.rb +0 -48
- data/test/prerecorded_trace_test.rb +0 -76
- data/test/trace_test.rb +0 -92
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'appmap/event'
|
|
4
|
+
|
|
1
5
|
module AppMap
|
|
2
6
|
module Rails
|
|
3
7
|
module ActionHandler
|
|
@@ -12,11 +16,11 @@ module AppMap
|
|
|
12
16
|
class HTTPServerRequest
|
|
13
17
|
include ContextKey
|
|
14
18
|
|
|
15
|
-
class Call < AppMap::
|
|
19
|
+
class Call < AppMap::Event::MethodEvent
|
|
16
20
|
attr_accessor :payload
|
|
17
21
|
|
|
18
22
|
def initialize(path, lineno, payload)
|
|
19
|
-
super AppMap::
|
|
23
|
+
super AppMap::Event.next_id_counter, :call, HTTPServerRequest, :call, path, lineno, false, Thread.current.object_id
|
|
20
24
|
|
|
21
25
|
self.payload = payload
|
|
22
26
|
end
|
|
@@ -45,18 +49,18 @@ module AppMap
|
|
|
45
49
|
def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
|
|
46
50
|
event = Call.new(__FILE__, __LINE__, payload)
|
|
47
51
|
Thread.current[context_key] = Context.new(event.id, Time.now)
|
|
48
|
-
AppMap
|
|
52
|
+
AppMap.tracing.record_event(event)
|
|
49
53
|
end
|
|
50
54
|
end
|
|
51
55
|
|
|
52
56
|
class HTTPServerResponse
|
|
53
57
|
include ContextKey
|
|
54
58
|
|
|
55
|
-
class Call < AppMap::
|
|
59
|
+
class Call < AppMap::Event::MethodReturnIgnoreValue
|
|
56
60
|
attr_accessor :payload
|
|
57
61
|
|
|
58
62
|
def initialize(path, lineno, payload, parent_id, elapsed)
|
|
59
|
-
super AppMap::
|
|
63
|
+
super AppMap::Event.next_id_counter, :return, HTTPServerResponse, :call, path, lineno, false, Thread.current.object_id
|
|
60
64
|
|
|
61
65
|
self.payload = payload
|
|
62
66
|
self.parent_id = parent_id
|
|
@@ -79,7 +83,7 @@ module AppMap
|
|
|
79
83
|
Thread.current[context_key] = nil
|
|
80
84
|
|
|
81
85
|
event = Call.new(__FILE__, __LINE__, payload, context.id, Time.now - context.start_time)
|
|
82
|
-
AppMap
|
|
86
|
+
AppMap.tracing.record_event(event)
|
|
83
87
|
end
|
|
84
88
|
end
|
|
85
89
|
end
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'appmap/event'
|
|
2
4
|
|
|
3
5
|
module AppMap
|
|
4
6
|
module Rails
|
|
5
7
|
class SQLHandler
|
|
6
|
-
class SQLCall < AppMap::
|
|
8
|
+
class SQLCall < AppMap::Event::MethodEvent
|
|
7
9
|
attr_accessor :payload
|
|
8
10
|
|
|
9
11
|
def initialize(path, lineno, payload)
|
|
10
|
-
super AppMap::
|
|
12
|
+
super AppMap::Event.next_id_counter, :call, SQLHandler, :call, path, lineno, false, Thread.current.object_id
|
|
11
13
|
|
|
12
14
|
self.payload = payload
|
|
13
15
|
end
|
|
@@ -26,9 +28,9 @@ module AppMap
|
|
|
26
28
|
end
|
|
27
29
|
end
|
|
28
30
|
|
|
29
|
-
class SQLReturn < AppMap::
|
|
31
|
+
class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
|
|
30
32
|
def initialize(path, lineno, parent_id, elapsed)
|
|
31
|
-
super AppMap::
|
|
33
|
+
super AppMap::Event.next_id_counter, :return, SQLHandler, :call, path, lineno, false, Thread.current.object_id
|
|
32
34
|
|
|
33
35
|
self.parent_id = parent_id
|
|
34
36
|
self.elapsed = elapsed
|
|
@@ -92,7 +94,7 @@ module AppMap
|
|
|
92
94
|
end
|
|
93
95
|
|
|
94
96
|
def call(_, started, finished, _, payload) # (name, started, finished, unique_id, payload)
|
|
95
|
-
return if AppMap
|
|
97
|
+
return if AppMap.tracing.empty?
|
|
96
98
|
|
|
97
99
|
reentry_key = "#{self.class.name}#call"
|
|
98
100
|
return if Thread.current[reentry_key] == true
|
|
@@ -135,8 +137,8 @@ module AppMap
|
|
|
135
137
|
SQLExaminer.examine payload, sql: sql
|
|
136
138
|
|
|
137
139
|
call = SQLCall.new(__FILE__, __LINE__, payload)
|
|
138
|
-
AppMap
|
|
139
|
-
AppMap
|
|
140
|
+
AppMap.tracing.record_event(call)
|
|
141
|
+
AppMap.tracing.record_event(SQLReturn.new(__FILE__, __LINE__, call.id, finished - started))
|
|
140
142
|
ensure
|
|
141
143
|
Thread.current[reentry_key] = nil
|
|
142
144
|
end
|
data/lib/appmap/railtie.rb
CHANGED
|
@@ -1,32 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module AppMap
|
|
4
|
+
# Railtie connects the AppMap recorder to Rails-specific features.
|
|
2
5
|
class Railtie < ::Rails::Railtie
|
|
3
6
|
config.appmap = ActiveSupport::OrderedOptions.new
|
|
4
7
|
|
|
5
|
-
initializer 'appmap.
|
|
8
|
+
initializer 'appmap.init' do |_| # params: app
|
|
9
|
+
AppMap.configure
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# appmap.subscribe subscribes to ActiveSupport Notifications so that they can be recorded as
|
|
13
|
+
# AppMap events.
|
|
14
|
+
initializer 'appmap.subscribe', after: 'appmap.init' do |_| # params: app
|
|
15
|
+
require 'appmap/rails/sql_handler'
|
|
16
|
+
require 'appmap/rails/action_handler'
|
|
17
|
+
ActiveSupport::Notifications.subscribe 'sql.sequel', AppMap::Rails::SQLHandler.new
|
|
18
|
+
ActiveSupport::Notifications.subscribe 'sql.active_record', AppMap::Rails::SQLHandler.new
|
|
19
|
+
ActiveSupport::Notifications.subscribe \
|
|
20
|
+
'start_processing.action_controller', AppMap::Rails::ActionHandler::HTTPServerRequest.new
|
|
21
|
+
ActiveSupport::Notifications.subscribe \
|
|
22
|
+
'process_action.action_controller', AppMap::Rails::ActionHandler::HTTPServerResponse.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# appmap.trace begins recording an AppMap trace and writes it to appmap.json.
|
|
26
|
+
# This behavior is only activated if the configuration setting app.config.appmap.enabled
|
|
27
|
+
# is truthy.
|
|
28
|
+
initializer 'appmap.trace', after: 'appmap.subscribe' do |app|
|
|
6
29
|
lambda do
|
|
7
30
|
return unless app.config.appmap.enabled
|
|
8
31
|
|
|
9
|
-
require 'appmap'
|
|
10
|
-
require 'appmap/config'
|
|
11
|
-
config = AppMap::Config.load_from_file 'appmap.yml'
|
|
12
|
-
|
|
13
32
|
require 'appmap/command/record'
|
|
14
33
|
require 'json'
|
|
15
|
-
AppMap::Command::Record.new(
|
|
16
|
-
|
|
34
|
+
AppMap::Command::Record.new(AppMap.configuration).perform do |version, metadata, class_map, events|
|
|
35
|
+
appmap = JSON.generate \
|
|
36
|
+
version: version,
|
|
37
|
+
metadata: metadata,
|
|
38
|
+
classMap: class_map,
|
|
39
|
+
events: events
|
|
40
|
+
File.open('appmap.json', 'w').write(appmap)
|
|
17
41
|
end
|
|
18
42
|
end.call
|
|
19
43
|
end
|
|
20
|
-
|
|
21
|
-
initializer 'appmap.subscribe', after: 'appmap.trace' do |_| # params: app
|
|
22
|
-
lambda do
|
|
23
|
-
require 'appmap/rails/sql_handler'
|
|
24
|
-
require 'appmap/rails/action_handler'
|
|
25
|
-
ActiveSupport::Notifications.subscribe('sql.sequel', AppMap::Rails::SQLHandler.new)
|
|
26
|
-
ActiveSupport::Notifications.subscribe('sql.active_record', AppMap::Rails::SQLHandler.new)
|
|
27
|
-
ActiveSupport::Notifications.subscribe('start_processing.action_controller', AppMap::Rails::ActionHandler::HTTPServerRequest.new)
|
|
28
|
-
ActiveSupport::Notifications.subscribe('process_action.action_controller', AppMap::Rails::ActionHandler::HTTPServerResponse.new)
|
|
29
|
-
end.call
|
|
30
|
-
end
|
|
31
44
|
end
|
|
32
45
|
end
|
data/lib/appmap/rspec.rb
CHANGED
|
@@ -1,342 +1,319 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'appmap'
|
|
4
|
-
require 'appmap/config'
|
|
5
|
-
require 'appmap/inspect'
|
|
6
|
-
require 'appmap/trace/tracer'
|
|
7
|
-
|
|
8
|
-
require 'active_support/inflector/transliterate'
|
|
9
|
-
|
|
10
3
|
module AppMap
|
|
11
4
|
# Integration of AppMap with RSpec. When enabled with APPMAP=true, the AppMap tracer will
|
|
12
5
|
# be activated around each scenario which has the metadata key `:appmap`.
|
|
13
6
|
module RSpec
|
|
14
7
|
APPMAP_OUTPUT_DIR = 'tmp/appmap/rspec'
|
|
8
|
+
LOG = false
|
|
15
9
|
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
def self.metadata
|
|
11
|
+
require 'appmap/command/record'
|
|
12
|
+
@metadata ||= AppMap::Command::Record.detect_metadata
|
|
13
|
+
@metadata.freeze
|
|
14
|
+
@metadata.dup
|
|
15
|
+
end
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
module FeatureAnnotations
|
|
18
|
+
def feature
|
|
19
|
+
return nil unless annotations
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
annotations[:feature]
|
|
22
|
+
end
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
def labels
|
|
25
|
+
labels = metadata[:appmap]
|
|
26
|
+
if labels.is_a?(Array)
|
|
27
|
+
labels
|
|
28
|
+
elsif labels.is_a?(String) || labels.is_a?(Symbol)
|
|
29
|
+
[ labels ]
|
|
30
|
+
else
|
|
31
|
+
[]
|
|
32
|
+
end
|
|
26
33
|
end
|
|
27
34
|
|
|
28
|
-
def
|
|
29
|
-
|
|
35
|
+
def feature_group
|
|
36
|
+
return nil unless annotations
|
|
37
|
+
|
|
38
|
+
annotations[:feature_group]
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
m[:feature] = feature_name if feature_name
|
|
38
|
-
m[:feature_group] = feature_group_name if feature_group_name
|
|
39
|
-
m[:labels] = labels if labels
|
|
40
|
-
m[:frameworks] ||= []
|
|
41
|
-
m[:frameworks] << {
|
|
42
|
-
name: 'rspec',
|
|
43
|
-
version: Gem.loaded_specs['rspec-core']&.version&.to_s
|
|
44
|
-
}
|
|
45
|
-
m[:recorder] = {
|
|
46
|
-
name: 'rspec'
|
|
47
|
-
}
|
|
41
|
+
def annotations
|
|
42
|
+
metadata.tap do |md|
|
|
43
|
+
description_args_hashes.each do |h|
|
|
44
|
+
md.merge! h
|
|
45
|
+
end
|
|
48
46
|
end
|
|
49
|
-
|
|
50
|
-
appmap = {
|
|
51
|
-
version: AppMap::APPMAP_FORMAT_VERSION,
|
|
52
|
-
classMap: features,
|
|
53
|
-
metadata: metadata,
|
|
54
|
-
events: events
|
|
55
|
-
}.compact
|
|
56
|
-
fname = sanitize_filename(example_name)
|
|
57
|
-
File.write(File.join(APPMAP_OUTPUT_DIR, "#{fname}.appmap.json"), JSON.generate(appmap))
|
|
58
47
|
end
|
|
59
48
|
|
|
60
|
-
|
|
61
|
-
# https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
|
|
62
|
-
def sanitize_filename(fname, separator: '_')
|
|
63
|
-
# Replace accented chars with their ASCII equivalents.
|
|
64
|
-
fname = fname.encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
|
|
49
|
+
protected
|
|
65
50
|
|
|
66
|
-
|
|
67
|
-
|
|
51
|
+
def metadata
|
|
52
|
+
return {} unless example_obj.respond_to?(:metadata)
|
|
68
53
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
|
|
54
|
+
example_obj.metadata
|
|
55
|
+
end
|
|
72
56
|
|
|
73
|
-
|
|
74
|
-
|
|
57
|
+
def description_args_hashes
|
|
58
|
+
return [] unless example_obj.respond_to?(:metadata)
|
|
75
59
|
|
|
76
|
-
|
|
77
|
-
fname.gsub(re_leading_trailing_separator, '')
|
|
60
|
+
(example_obj.metadata[:description_args] || []).select { |arg| arg.is_a?(Hash) }
|
|
78
61
|
end
|
|
79
62
|
end
|
|
80
63
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
64
|
+
# ScopeExample and ScopeExampleGroup is a way to handle the weird way that RSpec
|
|
65
|
+
# stores the nested example names.
|
|
66
|
+
ScopeExample = Struct.new(:example) do
|
|
67
|
+
include FeatureAnnotations
|
|
85
68
|
|
|
86
|
-
|
|
87
|
-
end
|
|
69
|
+
alias_method :example_obj, :example
|
|
88
70
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
labels
|
|
93
|
-
elsif labels.is_a?(String) || labels.is_a?(Symbol)
|
|
94
|
-
[ labels ]
|
|
95
|
-
else
|
|
96
|
-
[]
|
|
97
|
-
end
|
|
98
|
-
end
|
|
71
|
+
def description?
|
|
72
|
+
true
|
|
73
|
+
end
|
|
99
74
|
|
|
100
|
-
|
|
101
|
-
|
|
75
|
+
def description
|
|
76
|
+
example.description
|
|
77
|
+
end
|
|
102
78
|
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
def parent
|
|
80
|
+
ScopeExampleGroup.new(example.example_group)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
105
83
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
|
-
end
|
|
84
|
+
# As you can see here, the way that RSpec stores the example description and
|
|
85
|
+
# represents the example group hierarchy is pretty weird.
|
|
86
|
+
ScopeExampleGroup = Struct.new(:example_group) do
|
|
87
|
+
include FeatureAnnotations
|
|
113
88
|
|
|
114
|
-
|
|
89
|
+
alias_method :example_obj, :example_group
|
|
115
90
|
|
|
116
|
-
|
|
117
|
-
|
|
91
|
+
def description_args
|
|
92
|
+
# Don't stringify any hashes that RSpec considers part of the example group description.
|
|
93
|
+
example_group.metadata[:description_args].reject { |arg| arg.is_a?(Hash) }
|
|
94
|
+
end
|
|
118
95
|
|
|
119
|
-
|
|
120
|
-
|
|
96
|
+
def description?
|
|
97
|
+
return true if example_group.respond_to?(:described_class) && example_group.described_class
|
|
121
98
|
|
|
122
|
-
|
|
123
|
-
return [] unless example_obj.respond_to?(:metadata)
|
|
99
|
+
return true if example_group.respond_to?(:description) && !description_args.empty?
|
|
124
100
|
|
|
125
|
-
|
|
126
|
-
end
|
|
101
|
+
false
|
|
127
102
|
end
|
|
128
103
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
include FeatureAnnotations
|
|
133
|
-
|
|
134
|
-
alias_method :example_obj, :example
|
|
104
|
+
def description
|
|
105
|
+
description? ? description_args.join(' ') : nil
|
|
106
|
+
end
|
|
135
107
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
108
|
+
def parent
|
|
109
|
+
# An example group always has a parent; but it might be 'self'...
|
|
110
|
+
example_group.parent != example_group ? ScopeExampleGroup.new(example_group.parent) : nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
139
113
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
114
|
+
Recording = Struct.new(:example) do
|
|
115
|
+
def initialize(example)
|
|
116
|
+
super
|
|
143
117
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
end
|
|
118
|
+
warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
|
|
119
|
+
@trace = AppMap.tracing.trace
|
|
147
120
|
end
|
|
148
121
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ScopeExampleGroup = Struct.new(:example_group) do
|
|
152
|
-
include FeatureAnnotations
|
|
122
|
+
def finish
|
|
123
|
+
warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
|
|
153
124
|
|
|
154
|
-
|
|
125
|
+
events = []
|
|
126
|
+
AppMap.tracing.delete @trace
|
|
155
127
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
end
|
|
128
|
+
events << @trace.next_event.to_h while @trace.event?
|
|
129
|
+
|
|
130
|
+
AppMap::RSpec.add_event_methods @trace.event_methods
|
|
160
131
|
|
|
161
|
-
|
|
162
|
-
return true if example_group.respond_to?(:described_class) && example_group.described_class
|
|
132
|
+
class_map = AppMap.class_map(AppMap::RSpec.config, @trace.event_methods)
|
|
163
133
|
|
|
164
|
-
|
|
134
|
+
description = []
|
|
135
|
+
scope = ScopeExample.new(example)
|
|
136
|
+
feature_group = feature = nil
|
|
165
137
|
|
|
166
|
-
|
|
138
|
+
labels = []
|
|
139
|
+
while scope
|
|
140
|
+
labels += scope.labels
|
|
141
|
+
description << scope.description
|
|
142
|
+
feature ||= scope.feature
|
|
143
|
+
feature_group ||= scope.feature_group
|
|
144
|
+
scope = scope.parent
|
|
167
145
|
end
|
|
168
146
|
|
|
169
|
-
|
|
170
|
-
|
|
147
|
+
labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
|
|
148
|
+
description.reject!(&:nil?).reject(&:blank?)
|
|
149
|
+
default_description = description.last
|
|
150
|
+
description.reverse!
|
|
151
|
+
|
|
152
|
+
normalize = lambda do |desc|
|
|
153
|
+
desc.gsub('it should behave like', '')
|
|
154
|
+
.gsub(/Controller$/, '')
|
|
155
|
+
.gsub(/\s+/, ' ')
|
|
156
|
+
.strip
|
|
171
157
|
end
|
|
172
158
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
159
|
+
full_description = normalize.call(description.join(' '))
|
|
160
|
+
|
|
161
|
+
compute_feature_name = lambda do
|
|
162
|
+
return 'unknown' if description.empty?
|
|
163
|
+
|
|
164
|
+
feature_description = description.dup
|
|
165
|
+
num_tokens = [2, feature_description.length - 1].min
|
|
166
|
+
feature_description[0...num_tokens].map(&:strip).join(' ')
|
|
176
167
|
end
|
|
168
|
+
|
|
169
|
+
feature_group ||= normalize.call(default_description).underscore.gsub('/', '_').humanize
|
|
170
|
+
feature_name = feature || compute_feature_name.call if feature_group
|
|
171
|
+
feature_name = normalize.call(feature_name) if feature_name
|
|
172
|
+
|
|
173
|
+
AppMap::RSpec.save full_description,
|
|
174
|
+
class_map,
|
|
175
|
+
events: events,
|
|
176
|
+
feature_name: feature_name,
|
|
177
|
+
feature_group_name: feature_group,
|
|
178
|
+
labels: labels.blank? ? nil : labels
|
|
177
179
|
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
@recordings_by_example = {}
|
|
183
|
+
@config = nil
|
|
184
|
+
@event_methods = Set.new
|
|
185
|
+
|
|
186
|
+
class << self
|
|
187
|
+
def init
|
|
188
|
+
warn 'Configuring AppMap recorder for RSpec'
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
# some of which will fail.
|
|
185
|
-
#
|
|
186
|
-
# For example, ActiveRecord in Rails 4 defines #inspect (and
|
|
187
|
-
# therefore #to_s) in such a way that it will fail if called
|
|
188
|
-
# here.
|
|
189
|
-
tp.event == :call &&
|
|
190
|
-
tp.method_id == :subclass &&
|
|
191
|
-
tp.defined_class.singleton_class? &&
|
|
192
|
-
tp.defined_class.to_s == '#<Class:RSpec::Core::ExampleGroup>'
|
|
190
|
+
FileUtils.mkdir_p APPMAP_OUTPUT_DIR
|
|
191
|
+
|
|
192
|
+
require 'appmap/hook'
|
|
193
|
+
@config = AppMap.configure
|
|
194
|
+
AppMap::Hook.hook(@config)
|
|
193
195
|
end
|
|
194
196
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
tp.method_id == :initialize &&
|
|
198
|
-
tp.defined_class.to_s == 'RSpec::Core::Example'
|
|
197
|
+
def begin_spec(example)
|
|
198
|
+
@recordings_by_example[example.object_id] = Recording.new(example)
|
|
199
199
|
end
|
|
200
200
|
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
201
|
+
def end_spec(example)
|
|
202
|
+
recording = @recordings_by_example.delete(example.object_id)
|
|
203
|
+
return warn "No recording found for #{example}" unless recording
|
|
204
|
+
|
|
205
|
+
recording.finish
|
|
205
206
|
end
|
|
206
207
|
|
|
207
|
-
def
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
require 'set'
|
|
212
|
-
# file:lineno at which an Example block begins
|
|
213
|
-
trace_block_start = Set.new
|
|
214
|
-
# file:lineno at which an Example block ends
|
|
215
|
-
trace_block_end = Set.new
|
|
216
|
-
|
|
217
|
-
# value: a BlockParseNode from an RSpec file
|
|
218
|
-
# key: file:lineno at which the block begins
|
|
219
|
-
rspec_blocks = {}
|
|
220
|
-
|
|
221
|
-
# value: an Example instance
|
|
222
|
-
# key: file:lineno at which the Example block ends
|
|
223
|
-
examples = {}
|
|
224
|
-
|
|
225
|
-
current_tracer = nil
|
|
226
|
-
|
|
227
|
-
TracePoint.trace(:call, :b_call, :b_return) do |tp|
|
|
228
|
-
# When a new ExampleGroup is encountered, parse the source file containing it and look
|
|
229
|
-
# for blocks that might be Examples. Index each BlockParseNode by the start file:lineno.
|
|
230
|
-
if is_example_group_subclass_call?(tp)
|
|
231
|
-
example_block = tp.binding.eval('example_group_block')
|
|
232
|
-
source_path, start_line = example_block.source_location
|
|
233
|
-
require 'appmap/rspec/parser'
|
|
234
|
-
nodes, = AppMap::RSpec::Parser.new(file_path: source_path).parse
|
|
235
|
-
nodes.each do |node|
|
|
236
|
-
start_loc = [ node.file_path, node.first_line ].join(':')
|
|
237
|
-
rspec_blocks[start_loc] = node
|
|
238
|
-
end
|
|
239
|
-
end
|
|
208
|
+
def config
|
|
209
|
+
@config or raise "AppMap is not configured"
|
|
210
|
+
end
|
|
240
211
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
example_block = tp.binding.eval('example_block')
|
|
245
|
-
if example_block
|
|
246
|
-
source_path, start_line = example_block.source_location
|
|
247
|
-
start_loc = [ source_path, start_line ].join(':')
|
|
248
|
-
if (rspec_block = rspec_blocks[start_loc])
|
|
249
|
-
end_loc = [ source_path, rspec_block.last_line ].join(':')
|
|
250
|
-
trace_block_start << start_loc.tap { |loc| puts "Start: #{loc}" if LOG }
|
|
251
|
-
trace_block_end << end_loc.tap { |loc| puts "End: #{loc}" if LOG }
|
|
252
|
-
examples[end_loc] = tp.binding.eval('self')
|
|
253
|
-
end
|
|
254
|
-
end
|
|
255
|
-
end
|
|
212
|
+
def add_event_methods(event_methods)
|
|
213
|
+
@event_methods += event_methods
|
|
214
|
+
end
|
|
256
215
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
216
|
+
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|
|
|
218
|
+
m[:name] = example_name
|
|
219
|
+
m[:app] = @config.name
|
|
220
|
+
m[:feature] = feature_name if feature_name
|
|
221
|
+
m[:feature_group] = feature_group_name if feature_group_name
|
|
222
|
+
m[:labels] = labels if labels
|
|
223
|
+
m[:frameworks] ||= []
|
|
224
|
+
m[:frameworks] << {
|
|
225
|
+
name: 'rspec',
|
|
226
|
+
version: Gem.loaded_specs['rspec-core']&.version&.to_s
|
|
227
|
+
}
|
|
228
|
+
m[:recorder] = {
|
|
229
|
+
name: 'rspec'
|
|
230
|
+
}
|
|
231
|
+
end
|
|
260
232
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
233
|
+
appmap = {
|
|
234
|
+
version: AppMap::APPMAP_FORMAT_VERSION,
|
|
235
|
+
metadata: metadata,
|
|
236
|
+
classMap: class_map,
|
|
237
|
+
events: events
|
|
238
|
+
}.compact
|
|
239
|
+
fname = sanitize_filename(example_name)
|
|
267
240
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# AppMap file.
|
|
271
|
-
if current_tracer && tp.event == :b_return && trace_block_end.member?(loc)
|
|
272
|
-
puts "Ending trace on #{loc}" if LOG
|
|
273
|
-
events = []
|
|
274
|
-
AppMap::Trace.tracers.delete current_tracer
|
|
241
|
+
File.write(File.join(APPMAP_OUTPUT_DIR, "#{fname}.appmap.json"), JSON.generate(appmap))
|
|
242
|
+
end
|
|
275
243
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
244
|
+
def print_inventory
|
|
245
|
+
class_map = AppMap.class_map(@config, @event_methods)
|
|
246
|
+
save 'Inventory', class_map, labels: %w[inventory]
|
|
247
|
+
end
|
|
279
248
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
feature_group = feature = nil
|
|
284
|
-
|
|
285
|
-
labels = []
|
|
286
|
-
while scope
|
|
287
|
-
labels += scope.labels
|
|
288
|
-
description << scope.description
|
|
289
|
-
feature ||= scope.feature
|
|
290
|
-
feature_group ||= scope.feature_group
|
|
291
|
-
scope = scope.parent
|
|
292
|
-
end
|
|
249
|
+
def enabled?
|
|
250
|
+
ENV['APPMAP'] == 'true'
|
|
251
|
+
end
|
|
293
252
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
253
|
+
def run
|
|
254
|
+
init
|
|
255
|
+
at_exit do
|
|
256
|
+
print_inventory
|
|
257
|
+
end
|
|
258
|
+
end
|
|
298
259
|
|
|
299
|
-
|
|
300
|
-
desc.gsub('it should behave like', '')
|
|
301
|
-
.gsub(/Controller$/, '')
|
|
302
|
-
.gsub(/\s+/, ' ')
|
|
303
|
-
.strip
|
|
304
|
-
end
|
|
260
|
+
private
|
|
305
261
|
|
|
306
|
-
|
|
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: '_')
|
|
307
267
|
|
|
308
|
-
|
|
309
|
-
|
|
268
|
+
# Turn unwanted chars into the separator.
|
|
269
|
+
fname.gsub!(/[^a-z0-9\-_]+/i, separator)
|
|
310
270
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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)
|
|
315
277
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
278
|
+
# Finally, Remove leading/trailing separator.
|
|
279
|
+
fname.gsub(re_leading_trailing_separator, '')
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
if AppMap::RSpec.enabled?
|
|
286
|
+
require 'appmap'
|
|
287
|
+
require 'active_support/inflector/transliterate'
|
|
288
|
+
require 'rspec/core'
|
|
289
|
+
require 'rspec/core/example'
|
|
319
290
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
291
|
+
module RSpec
|
|
292
|
+
module Core
|
|
293
|
+
class Example
|
|
294
|
+
class << self
|
|
295
|
+
def wrap_example_block(example, fn)
|
|
296
|
+
proc do
|
|
297
|
+
AppMap::RSpec.begin_spec example
|
|
298
|
+
begin
|
|
299
|
+
instance_exec(&fn)
|
|
300
|
+
ensure
|
|
301
|
+
AppMap::RSpec.end_spec example
|
|
302
|
+
end
|
|
325
303
|
end
|
|
326
304
|
end
|
|
327
305
|
end
|
|
328
|
-
end
|
|
329
306
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
307
|
+
def self.new(*arguments, &block)
|
|
308
|
+
warn "Wrapping example_block for #{name}" if AppMap::RSpec::LOG
|
|
309
|
+
allocate.tap do |obj|
|
|
310
|
+
arguments[arguments.length - 1] = wrap_example_block(obj, arguments.last) if arguments.last.is_a?(Proc)
|
|
311
|
+
obj.send :initialize, *arguments, &block
|
|
312
|
+
end
|
|
313
|
+
end
|
|
337
314
|
end
|
|
338
315
|
end
|
|
339
316
|
end
|
|
340
|
-
end
|
|
341
317
|
|
|
342
|
-
AppMap::RSpec.run
|
|
318
|
+
AppMap::RSpec.run
|
|
319
|
+
end
|