appmap 0.23.0 → 0.27.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 +2 -0
- data/.rubocop.yml +17 -8
- data/.travis.yml +6 -0
- data/CHANGELOG.md +43 -0
- data/README.md +33 -21
- data/Rakefile +3 -3
- data/appmap.gemspec +3 -1
- data/exe/appmap +5 -73
- data/lib/appmap.rb +61 -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/event.rb +168 -0
- data/lib/appmap/hook.rb +152 -0
- data/lib/appmap/middleware/remote_recording.rb +14 -21
- data/lib/appmap/rails/action_handler.rb +10 -6
- data/lib/appmap/rails/sql_handler.rb +10 -13
- data/lib/appmap/railtie.rb +31 -18
- data/lib/appmap/rspec.rb +247 -260
- 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 +369 -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 +5 -0
- data/test/cli_test.rb +4 -46
- 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/fixtures/rspec_recorder/Gemfile +1 -1
- data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
- data/test/rspec_test.rb +5 -0
- data/test/test_helper.rb +0 -42
- metadata +46 -63
- data/exe/_appmap-record-self +0 -49
- data/lib/appmap/command/inspect.rb +0 -14
- data/lib/appmap/command/upload.rb +0 -99
- 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
|
@@ -4,17 +4,13 @@ module AppMap
|
|
|
4
4
|
module Middleware
|
|
5
5
|
# RemoteRecording adds `/_appmap/record` routes to control recordings via HTTP requests
|
|
6
6
|
class RemoteRecording
|
|
7
|
-
|
|
8
7
|
def initialize(app)
|
|
9
8
|
require 'appmap/command/record'
|
|
10
|
-
require 'appmap/command/upload'
|
|
11
|
-
require 'appmap/trace/tracer'
|
|
12
|
-
require 'appmap/config'
|
|
13
9
|
require 'json'
|
|
14
10
|
|
|
15
11
|
@app = app
|
|
16
|
-
@
|
|
17
|
-
|
|
12
|
+
@config = AppMap.configure
|
|
13
|
+
AppMap::Hook.hook(@config)
|
|
18
14
|
end
|
|
19
15
|
|
|
20
16
|
def event_loop
|
|
@@ -29,23 +25,23 @@ module AppMap
|
|
|
29
25
|
end
|
|
30
26
|
|
|
31
27
|
def start_recording
|
|
32
|
-
return [
|
|
28
|
+
return [ 409, 'Recording is already in progress' ] if @tracer
|
|
33
29
|
|
|
34
30
|
@events = []
|
|
35
|
-
@tracer = AppMap
|
|
31
|
+
@tracer = AppMap.tracing.trace
|
|
36
32
|
@event_thread = Thread.new { event_loop }
|
|
37
33
|
@event_thread.abort_on_exception = true
|
|
38
34
|
|
|
39
|
-
[
|
|
35
|
+
[ 200 ]
|
|
40
36
|
end
|
|
41
37
|
|
|
42
38
|
def stop_recording(req)
|
|
43
|
-
return [
|
|
39
|
+
return [ 404, 'No recording is in progress' ] unless @tracer
|
|
44
40
|
|
|
45
41
|
tracer = @tracer
|
|
46
42
|
@tracer = nil
|
|
47
43
|
|
|
48
|
-
AppMap
|
|
44
|
+
AppMap.tracing.delete(tracer)
|
|
49
45
|
|
|
50
46
|
@event_thread.exit
|
|
51
47
|
@event_thread.join
|
|
@@ -73,9 +69,13 @@ module AppMap
|
|
|
73
69
|
name: 'remote_recording'
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
response = JSON.generate
|
|
72
|
+
response = JSON.generate \
|
|
73
|
+
version: AppMap::APPMAP_FORMAT_VERSION,
|
|
74
|
+
classMap: AppMap.class_map(@config, tracer.event_methods),
|
|
75
|
+
metadata: metadata,
|
|
76
|
+
events: @events
|
|
77
77
|
|
|
78
|
-
[
|
|
78
|
+
[ 200, response ]
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
def call(env)
|
|
@@ -103,20 +103,13 @@ module AppMap
|
|
|
103
103
|
[ 404, '' ]
|
|
104
104
|
end
|
|
105
105
|
|
|
106
|
-
status
|
|
107
|
-
status = 500 if status == false
|
|
108
|
-
|
|
109
|
-
[status, { 'Content-Type' => 'application/text' }, [body || '']]
|
|
106
|
+
[status, { 'Content-Type' => 'application/json' }, [body || '']]
|
|
110
107
|
end
|
|
111
108
|
|
|
112
109
|
def html_response?(headers)
|
|
113
110
|
headers['Content-Type'] && headers['Content-Type'] =~ /html/
|
|
114
111
|
end
|
|
115
112
|
|
|
116
|
-
def config
|
|
117
|
-
@config ||= AppMap::Config.load_from_file 'appmap.yml'
|
|
118
|
-
end
|
|
119
|
-
|
|
120
113
|
def recording?
|
|
121
114
|
!@event_thread.nil?
|
|
122
115
|
end
|
|
@@ -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
|
|
@@ -100,9 +102,6 @@ module AppMap
|
|
|
100
102
|
Thread.current[reentry_key] = true
|
|
101
103
|
begin
|
|
102
104
|
sql = payload[:sql].strip
|
|
103
|
-
sql_upper = sql.upcase
|
|
104
|
-
|
|
105
|
-
return unless WHITELIST.find { |keyword| sql_upper.index(keyword) == 0 }
|
|
106
105
|
|
|
107
106
|
# Detect whether a function call within a specified filename is present in the call stack.
|
|
108
107
|
find_in_backtrace = lambda do |file_name, function_name = nil|
|
|
@@ -135,14 +134,12 @@ module AppMap
|
|
|
135
134
|
SQLExaminer.examine payload, sql: sql
|
|
136
135
|
|
|
137
136
|
call = SQLCall.new(__FILE__, __LINE__, payload)
|
|
138
|
-
AppMap
|
|
139
|
-
AppMap
|
|
137
|
+
AppMap.tracing.record_event(call)
|
|
138
|
+
AppMap.tracing.record_event(SQLReturn.new(__FILE__, __LINE__, call.id, finished - started))
|
|
140
139
|
ensure
|
|
141
140
|
Thread.current[reentry_key] = nil
|
|
142
141
|
end
|
|
143
142
|
end
|
|
144
|
-
|
|
145
|
-
WHITELIST = %w[SELECT INSERT UPDATE DELETE].freeze
|
|
146
143
|
end
|
|
147
144
|
end
|
|
148
145
|
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,329 @@
|
|
|
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.deep_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
|
|
104
|
+
def description
|
|
105
|
+
description? ? description_args.join(' ') : nil
|
|
106
|
+
end
|
|
133
107
|
|
|
134
|
-
|
|
108
|
+
def parent
|
|
109
|
+
# An example group always has a parent; but it might be 'self'...
|
|
135
110
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
111
|
+
# DEPRECATION WARNING: `Module#parent` has been renamed to `module_parent`. `parent` is deprecated and will be
|
|
112
|
+
# removed in Rails 6.1. (called from parent at /Users/kgilpin/source/appland/appmap-ruby/lib/appmap/rspec.rb:110)
|
|
113
|
+
example_group_parent = \
|
|
114
|
+
if example_group.respond_to?(:module_parent)
|
|
115
|
+
example_group.module_parent
|
|
116
|
+
else
|
|
117
|
+
example_group.parent
|
|
118
|
+
end
|
|
139
119
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
120
|
+
example_group_parent != example_group ? ScopeExampleGroup.new(example_group_parent) : nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
143
123
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
124
|
+
Recording = Struct.new(:example) do
|
|
125
|
+
def initialize(example)
|
|
126
|
+
super
|
|
127
|
+
|
|
128
|
+
warn "Starting recording of example #{example}" if AppMap::RSpec::LOG
|
|
129
|
+
@trace = AppMap.tracing.trace
|
|
147
130
|
end
|
|
148
131
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
ScopeExampleGroup = Struct.new(:example_group) do
|
|
152
|
-
include FeatureAnnotations
|
|
132
|
+
def finish
|
|
133
|
+
warn "Finishing recording of example #{example}" if AppMap::RSpec::LOG
|
|
153
134
|
|
|
154
|
-
|
|
135
|
+
events = []
|
|
136
|
+
AppMap.tracing.delete @trace
|
|
155
137
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
end
|
|
138
|
+
events << @trace.next_event.to_h while @trace.event?
|
|
139
|
+
|
|
140
|
+
AppMap::RSpec.add_event_methods @trace.event_methods
|
|
160
141
|
|
|
161
|
-
|
|
162
|
-
return true if example_group.respond_to?(:described_class) && example_group.described_class
|
|
142
|
+
class_map = AppMap.class_map(AppMap::RSpec.config, @trace.event_methods)
|
|
163
143
|
|
|
164
|
-
|
|
144
|
+
description = []
|
|
145
|
+
scope = ScopeExample.new(example)
|
|
146
|
+
feature_group = feature = nil
|
|
165
147
|
|
|
166
|
-
|
|
148
|
+
labels = []
|
|
149
|
+
while scope
|
|
150
|
+
labels += scope.labels
|
|
151
|
+
description << scope.description
|
|
152
|
+
feature ||= scope.feature
|
|
153
|
+
feature_group ||= scope.feature_group
|
|
154
|
+
scope = scope.parent
|
|
167
155
|
end
|
|
168
156
|
|
|
169
|
-
|
|
170
|
-
|
|
157
|
+
labels = labels.map(&:to_s).map(&:strip).reject(&:blank?).map(&:downcase).uniq
|
|
158
|
+
description.reject!(&:nil?).reject(&:blank?)
|
|
159
|
+
default_description = description.last
|
|
160
|
+
description.reverse!
|
|
161
|
+
|
|
162
|
+
normalize = lambda do |desc|
|
|
163
|
+
desc.gsub('it should behave like', '')
|
|
164
|
+
.gsub(/Controller$/, '')
|
|
165
|
+
.gsub(/\s+/, ' ')
|
|
166
|
+
.strip
|
|
171
167
|
end
|
|
172
168
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
169
|
+
full_description = normalize.call(description.join(' '))
|
|
170
|
+
|
|
171
|
+
compute_feature_name = lambda do
|
|
172
|
+
return 'unknown' if description.empty?
|
|
173
|
+
|
|
174
|
+
feature_description = description.dup
|
|
175
|
+
num_tokens = [2, feature_description.length - 1].min
|
|
176
|
+
feature_description[0...num_tokens].map(&:strip).join(' ')
|
|
176
177
|
end
|
|
178
|
+
|
|
179
|
+
feature_group ||= normalize.call(default_description).underscore.gsub('/', '_').humanize
|
|
180
|
+
feature_name = feature || compute_feature_name.call if feature_group
|
|
181
|
+
feature_name = normalize.call(feature_name) if feature_name
|
|
182
|
+
|
|
183
|
+
AppMap::RSpec.save full_description,
|
|
184
|
+
class_map,
|
|
185
|
+
events: events,
|
|
186
|
+
feature_name: feature_name,
|
|
187
|
+
feature_group_name: feature_group,
|
|
188
|
+
labels: labels.blank? ? nil : labels
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
@recordings_by_example = {}
|
|
193
|
+
@config = nil
|
|
194
|
+
@event_methods = Set.new
|
|
195
|
+
|
|
196
|
+
class << self
|
|
197
|
+
def init
|
|
198
|
+
warn 'Configuring AppMap recorder for RSpec'
|
|
199
|
+
|
|
200
|
+
FileUtils.mkdir_p APPMAP_OUTPUT_DIR
|
|
201
|
+
|
|
202
|
+
require 'appmap/hook'
|
|
203
|
+
@config = AppMap.configure
|
|
204
|
+
AppMap::Hook.hook(@config)
|
|
177
205
|
end
|
|
178
206
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def is_example_group_subclass_call?(tp)
|
|
182
|
-
# Order is important here. Checking for method_id == :subclass
|
|
183
|
-
# first will avoid calling defined_class.to_s in many cases,
|
|
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>'
|
|
207
|
+
def begin_spec(example)
|
|
208
|
+
@recordings_by_example[example.object_id] = Recording.new(example)
|
|
193
209
|
end
|
|
194
210
|
|
|
195
|
-
def
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
211
|
+
def end_spec(example)
|
|
212
|
+
recording = @recordings_by_example.delete(example.object_id)
|
|
213
|
+
return warn "No recording found for #{example}" unless recording
|
|
214
|
+
|
|
215
|
+
recording.finish
|
|
199
216
|
end
|
|
200
217
|
|
|
201
|
-
def
|
|
202
|
-
|
|
203
|
-
recorder.setup
|
|
204
|
-
end.save 'Inventory', labels: %w[inventory]
|
|
218
|
+
def config
|
|
219
|
+
@config or raise "AppMap is not configured"
|
|
205
220
|
end
|
|
206
221
|
|
|
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
|
|
222
|
+
def add_event_methods(event_methods)
|
|
223
|
+
@event_methods += event_methods
|
|
224
|
+
end
|
|
240
225
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
226
|
+
def save(example_name, class_map, events: nil, feature_name: nil, feature_group_name: nil, labels: nil)
|
|
227
|
+
metadata = RSpec.metadata.tap do |m|
|
|
228
|
+
m[:name] = example_name
|
|
229
|
+
m[:app] = @config.name
|
|
230
|
+
m[:feature] = feature_name if feature_name
|
|
231
|
+
m[:feature_group] = feature_group_name if feature_group_name
|
|
232
|
+
m[:labels] = labels if labels
|
|
233
|
+
m[:frameworks] ||= []
|
|
234
|
+
m[:frameworks] << {
|
|
235
|
+
name: 'rspec',
|
|
236
|
+
version: Gem.loaded_specs['rspec-core']&.version&.to_s
|
|
237
|
+
}
|
|
238
|
+
m[:recorder] = {
|
|
239
|
+
name: 'rspec'
|
|
240
|
+
}
|
|
241
|
+
end
|
|
256
242
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
243
|
+
appmap = {
|
|
244
|
+
version: AppMap::APPMAP_FORMAT_VERSION,
|
|
245
|
+
metadata: metadata,
|
|
246
|
+
classMap: class_map,
|
|
247
|
+
events: events
|
|
248
|
+
}.compact
|
|
249
|
+
fname = sanitize_filename(example_name)
|
|
260
250
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if tp.event == :b_call && trace_block_start.member?(loc)
|
|
264
|
-
puts "Starting trace on #{loc}" if LOG
|
|
265
|
-
current_tracer = AppMap::Trace.tracers.trace(recorder.functions)
|
|
266
|
-
end
|
|
251
|
+
File.write(File.join(APPMAP_OUTPUT_DIR, "#{fname}.appmap.json"), JSON.generate(appmap))
|
|
252
|
+
end
|
|
267
253
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
puts "Ending trace on #{loc}" if LOG
|
|
273
|
-
events = []
|
|
274
|
-
AppMap::Trace.tracers.delete current_tracer
|
|
254
|
+
def print_inventory
|
|
255
|
+
class_map = AppMap.class_map(@config, @event_methods)
|
|
256
|
+
save 'Inventory', class_map, labels: %w[inventory]
|
|
257
|
+
end
|
|
275
258
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
259
|
+
def enabled?
|
|
260
|
+
ENV['APPMAP'] == 'true'
|
|
261
|
+
end
|
|
279
262
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
|
263
|
+
def run
|
|
264
|
+
init
|
|
265
|
+
at_exit do
|
|
266
|
+
print_inventory
|
|
267
|
+
end
|
|
268
|
+
end
|
|
293
269
|
|
|
294
|
-
|
|
295
|
-
description.reject!(&:nil?).reject(&:blank?)
|
|
296
|
-
default_description = description.last
|
|
297
|
-
description.reverse!
|
|
270
|
+
private
|
|
298
271
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
end
|
|
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: '_')
|
|
305
277
|
|
|
306
|
-
|
|
278
|
+
# Turn unwanted chars into the separator.
|
|
279
|
+
fname.gsub!(/[^a-z0-9\-_]+/i, separator)
|
|
307
280
|
|
|
308
|
-
|
|
309
|
-
|
|
281
|
+
re_sep = Regexp.escape(separator)
|
|
282
|
+
re_duplicate_separator = /#{re_sep}{2,}/
|
|
283
|
+
re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
|
|
310
284
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
feature_description[0...num_tokens].map(&:strip).join(' ')
|
|
314
|
-
end
|
|
285
|
+
# No more than one of the separator in a row.
|
|
286
|
+
fname.gsub!(re_duplicate_separator, separator)
|
|
315
287
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
288
|
+
# Finally, Remove leading/trailing separator.
|
|
289
|
+
fname.gsub(re_leading_trailing_separator, '')
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if AppMap::RSpec.enabled?
|
|
296
|
+
require 'appmap'
|
|
297
|
+
require 'active_support/inflector/transliterate'
|
|
298
|
+
require 'rspec/core'
|
|
299
|
+
require 'rspec/core/example'
|
|
319
300
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
301
|
+
module RSpec
|
|
302
|
+
module Core
|
|
303
|
+
class Example
|
|
304
|
+
class << self
|
|
305
|
+
def wrap_example_block(example, fn)
|
|
306
|
+
proc do
|
|
307
|
+
AppMap::RSpec.begin_spec example
|
|
308
|
+
begin
|
|
309
|
+
instance_exec(&fn)
|
|
310
|
+
ensure
|
|
311
|
+
AppMap::RSpec.end_spec example
|
|
312
|
+
end
|
|
325
313
|
end
|
|
326
314
|
end
|
|
327
315
|
end
|
|
328
|
-
end
|
|
329
316
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
317
|
+
def self.new(*arguments, &block)
|
|
318
|
+
warn "Wrapping example_block for #{name}" if AppMap::RSpec::LOG
|
|
319
|
+
allocate.tap do |obj|
|
|
320
|
+
arguments[arguments.length - 1] = wrap_example_block(obj, arguments.last) if arguments.last.is_a?(Proc)
|
|
321
|
+
obj.send :initialize, *arguments, &block
|
|
322
|
+
end
|
|
323
|
+
end
|
|
337
324
|
end
|
|
338
325
|
end
|
|
339
326
|
end
|
|
340
|
-
end
|
|
341
327
|
|
|
342
|
-
AppMap::RSpec.run
|
|
328
|
+
AppMap::RSpec.run
|
|
329
|
+
end
|