appmap 0.25.2 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -2
  3. data/CHANGELOG.md +30 -0
  4. data/README.md +123 -39
  5. data/exe/appmap +3 -57
  6. data/lib/appmap.rb +51 -32
  7. data/lib/appmap/algorithm/stats.rb +2 -1
  8. data/lib/appmap/class_map.rb +1 -1
  9. data/lib/appmap/command/record.rb +2 -61
  10. data/lib/appmap/cucumber.rb +89 -0
  11. data/lib/appmap/event.rb +6 -6
  12. data/lib/appmap/hook.rb +27 -13
  13. data/lib/appmap/metadata.rb +62 -0
  14. data/lib/appmap/middleware/remote_recording.rb +2 -7
  15. data/lib/appmap/rails/action_handler.rb +2 -2
  16. data/lib/appmap/rails/sql_handler.rb +2 -2
  17. data/lib/appmap/railtie.rb +2 -2
  18. data/lib/appmap/rspec.rb +20 -38
  19. data/lib/appmap/trace.rb +11 -11
  20. data/lib/appmap/util.rb +40 -0
  21. data/lib/appmap/version.rb +1 -1
  22. data/package-lock.json +3 -3
  23. data/spec/abstract_controller4_base_spec.rb +1 -1
  24. data/spec/abstract_controller_base_spec.rb +1 -1
  25. data/spec/fixtures/hook/singleton_method.rb +54 -0
  26. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  27. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  28. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  29. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  30. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  31. data/spec/hook_spec.rb +107 -23
  32. data/spec/rails_spec_helper.rb +2 -0
  33. data/spec/rspec_feature_metadata_spec.rb +2 -0
  34. data/spec/spec_helper.rb +4 -0
  35. data/spec/util_spec.rb +21 -0
  36. data/test/cli_test.rb +2 -15
  37. data/test/cucumber_test.rb +72 -0
  38. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  39. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  40. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  41. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  42. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  43. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  44. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  45. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  46. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  47. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  48. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  49. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  50. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  51. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  52. data/test/fixtures/rspec_recorder/Gemfile +1 -1
  53. data/test/fixtures/rspec_recorder/spec/decorated_hello_spec.rb +12 -0
  54. data/test/rspec_test.rb +5 -0
  55. metadata +26 -4
  56. data/lib/appmap/command/upload.rb +0 -101
  57. data/spec/fixtures/hook/class_method.rb +0 -17
@@ -74,10 +74,11 @@ module AppMap
74
74
 
75
75
  class_name_func = ->(event) { event['defined_class'] }
76
76
  full_name_func = lambda do |event|
77
+ call = event['event'] == 'call'
77
78
  class_name = event['defined_class']
78
79
  static = event['static']
79
80
  function_name = event['method_id']
80
- [ class_name, static ? '.' : '#', function_name ].join if class_name && !static.nil? && function_name
81
+ [ class_name, static ? '.' : '#', function_name ].join if call && class_name && !static.nil? && function_name
81
82
  end
82
83
 
83
84
  class_frequency = frequency_calc.call(class_name_func)
@@ -95,7 +95,7 @@ module AppMap
95
95
  location_file, lineno = location
96
96
  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
97
97
 
98
- static = method.method.owner.singleton_class?
98
+ static = method.static
99
99
 
100
100
  object_infos = [
101
101
  {
@@ -5,66 +5,7 @@ module AppMap
5
5
  RecordStruct = Struct.new(:config, :program)
6
6
 
7
7
  class Record < RecordStruct
8
- class << self
9
- # Builds a Hash of metadata which can be detected by inspecting the system.
10
- def detect_metadata
11
- {
12
- language: {
13
- name: 'ruby',
14
- engine: RUBY_ENGINE,
15
- version: RUBY_VERSION
16
- },
17
- client: {
18
- name: 'appmap',
19
- url: AppMap::URL,
20
- version: AppMap::VERSION
21
- }
22
- }.tap do |m|
23
- if defined?(::Rails)
24
- m[:frameworks] ||= []
25
- m[:frameworks] << {
26
- name: 'rails',
27
- version: ::Rails.version
28
- }
29
- end
30
- m[:git] = git_metadata if git_available
31
- end
32
- end
33
-
34
- protected
35
-
36
- def git_available
37
- @git_available = system('git status 2>&1 > /dev/null') if @git_available.nil?
38
- end
39
-
40
- def git_metadata
41
- git_repo = `git config --get remote.origin.url`.strip
42
- git_branch = `git rev-parse --abbrev-ref HEAD`.strip
43
- git_sha = `git rev-parse HEAD`.strip
44
- git_status = `git status -s`.split("\n").map(&:strip)
45
- git_last_annotated_tag = `git describe --abbrev=0 2>/dev/null`.strip
46
- git_last_annotated_tag = nil if git_last_annotated_tag.blank?
47
- git_last_tag = `git describe --abbrev=0 --tags 2>/dev/null`.strip
48
- git_last_tag = nil if git_last_tag.blank?
49
- git_commits_since_last_annotated_tag = `git describe`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_annotated_tag
50
- git_commits_since_last_tag = `git describe --tags`.strip =~ /-(\d+)-(\w+)$/[1] rescue 0 if git_last_tag
51
-
52
- {
53
- repository: git_repo,
54
- branch: git_branch,
55
- commit: git_sha,
56
- status: git_status,
57
- git_last_annotated_tag: git_last_annotated_tag,
58
- git_last_tag: git_last_tag,
59
- git_commits_since_last_annotated_tag: git_commits_since_last_annotated_tag,
60
- git_commits_since_last_tag: git_commits_since_last_tag
61
- }
62
- end
63
- end
64
-
65
8
  def perform(&block)
66
- AppMap::Hook.hook(config)
67
-
68
9
  tracer = AppMap.tracing.trace
69
10
 
70
11
  events = []
@@ -85,8 +26,8 @@ module AppMap
85
26
  quit = true
86
27
  event_thread.join
87
28
  yield AppMap::APPMAP_FORMAT_VERSION,
88
- self.class.detect_metadata,
89
- AppMap.class_map(config, tracer.event_methods),
29
+ AppMap.detect_metadata,
30
+ AppMap.class_map(tracer.event_methods),
90
31
  events
91
32
  end
92
33
 
@@ -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
@@ -15,15 +15,13 @@ module AppMap
15
15
  end
16
16
  end
17
17
 
18
- MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :static, :thread_id)
18
+ MethodEventStruct = Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :thread_id)
19
19
 
20
20
  class MethodEvent < MethodEventStruct
21
21
  LIMIT = 100
22
22
 
23
23
  class << self
24
24
  def build_from_invocation(me, event_type, defined_class, method)
25
- singleton = method.owner.singleton_class?
26
-
27
25
  me.id = AppMap::Event.next_id_counter
28
26
  me.event = event_type
29
27
  me.defined_class = defined_class
@@ -32,7 +30,6 @@ module AppMap
32
30
  path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
33
31
  me.path = path
34
32
  me.lineno = method.source_location[1]
35
- me.static = singleton
36
33
  me.thread_id = Thread.current.object_id
37
34
  end
38
35
 
@@ -62,11 +59,10 @@ module AppMap
62
59
  end
63
60
  end
64
61
 
65
- alias static? static
66
62
  end
67
63
 
68
64
  class MethodCall < MethodEvent
69
- attr_accessor :parameters, :receiver
65
+ attr_accessor :parameters, :receiver, :static
70
66
 
71
67
  class << self
72
68
  def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
@@ -88,6 +84,7 @@ module AppMap
88
84
  object_id: receiver.__id__,
89
85
  value: display_string(receiver)
90
86
  }
87
+ mc.static = receiver.is_a?(Module)
91
88
  MethodEvent.build_from_invocation(mc, :call, defined_class, method)
92
89
  end
93
90
  end
@@ -95,10 +92,13 @@ module AppMap
95
92
 
96
93
  def to_h
97
94
  super.tap do |h|
95
+ h[:static] = static
98
96
  h[:parameters] = parameters
99
97
  h[:receiver] = receiver
100
98
  end
101
99
  end
100
+
101
+ alias static? static
102
102
  end
103
103
 
104
104
  class MethodReturnIgnoreValue < MethodEvent
@@ -106,16 +106,30 @@ module AppMap
106
106
  # Skip methods that have no instruction sequence, as they are obviously trivial.
107
107
  next unless disasm
108
108
 
109
- defined_class, method_symbol = \
110
- if method.owner.singleton_class?
111
- # Singleton class name is like: #<Class:<(.*)>>
112
- class_name = method.owner.to_s['#<Class:<'.length-1..-2]
113
- [ class_name, '.' ]
114
- else
115
- [ method.owner.name, '#' ]
116
- end
109
+ defined_class, method_symbol = if method.owner.singleton_class?
110
+ # Singleton class names can take two forms:
111
+ # #<Class:Foo> or
112
+ # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
113
+ # the class from the string.
114
+ #
115
+ # (There really isn't a better way to do this. The
116
+ # singleton's reference to the class it was created
117
+ # from is stored in an instance variable named
118
+ # '__attached__'. It doesn't have the '@' prefix, so
119
+ # it's internal only, and not accessible from user
120
+ # code.)
121
+ class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
122
+ [ class_name, '.' ]
123
+ else
124
+ [ method.owner.name, '#' ]
125
+ end
126
+
127
+ method_display_name = "#{defined_class}#{method_symbol}#{method.name}"
128
+ # Don't try and trace the tracing method or there will be a stack overflow
129
+ # in the defined hook method.
130
+ next if method_display_name == "AppMap.tracing"
117
131
 
118
- warn "AppMap: Hooking #{defined_class}#{method_symbol}#{method.name}" if LOG
132
+ warn "AppMap: Hooking #{method_display_name}" if LOG
119
133
 
120
134
  cls.define_method method_id do |*args, &block|
121
135
  base_method = method.bind(self).to_proc
@@ -141,11 +155,11 @@ module AppMap
141
155
  end
142
156
  end
143
157
  end
144
- end
145
-
146
- instance_methods.each(&hook_method.call(cls))
147
- class_methods.each(&hook_method.call(cls.singleton_class))
148
158
  end
159
+
160
+ instance_methods.each(&hook_method.call(cls))
161
+ class_methods.each(&hook_method.call(cls.singleton_class))
162
+ end
149
163
  end
150
164
  end
151
165
  end
@@ -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,13 +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
- require 'appmap/command/upload'
10
8
  require 'json'
11
9
 
12
10
  @app = app
13
- @config = AppMap.configure
14
- AppMap::Hook.hook(@config)
15
11
  end
16
12
 
17
13
  def event_loop
@@ -64,15 +60,14 @@ module AppMap
64
60
  @events.delete_if(&is_control_command_event)
65
61
  @events.delete_if(&is_return_from_control_command_event)
66
62
 
67
- require 'appmap/command/record'
68
- metadata = AppMap::Command::Record.detect_metadata
63
+ metadata = AppMap.detect_metadata
69
64
  metadata[:recorder] = {
70
65
  name: 'remote_recording'
71
66
  }
72
67
 
73
68
  response = JSON.generate \
74
69
  version: AppMap::APPMAP_FORMAT_VERSION,
75
- classMap: AppMap.class_map(@config, tracer.event_methods),
70
+ classMap: AppMap.class_map(tracer.event_methods),
76
71
  metadata: metadata,
77
72
  events: @events
78
73
 
@@ -20,7 +20,7 @@ module AppMap
20
20
  attr_accessor :payload
21
21
 
22
22
  def initialize(path, lineno, payload)
23
- super AppMap::Event.next_id_counter, :call, HTTPServerRequest, :call, path, lineno, false, Thread.current.object_id
23
+ super AppMap::Event.next_id_counter, :call, HTTPServerRequest, :call, path, lineno, Thread.current.object_id
24
24
 
25
25
  self.payload = payload
26
26
  end
@@ -60,7 +60,7 @@ module AppMap
60
60
  attr_accessor :payload
61
61
 
62
62
  def initialize(path, lineno, payload, parent_id, elapsed)
63
- super AppMap::Event.next_id_counter, :return, HTTPServerResponse, :call, path, lineno, false, Thread.current.object_id
63
+ super AppMap::Event.next_id_counter, :return, HTTPServerResponse, :call, path, lineno, Thread.current.object_id
64
64
 
65
65
  self.payload = payload
66
66
  self.parent_id = parent_id
@@ -9,7 +9,7 @@ module AppMap
9
9
  attr_accessor :payload
10
10
 
11
11
  def initialize(path, lineno, payload)
12
- super AppMap::Event.next_id_counter, :call, SQLHandler, :call, path, lineno, false, Thread.current.object_id
12
+ super AppMap::Event.next_id_counter, :call, SQLHandler, :call, path, lineno, Thread.current.object_id
13
13
 
14
14
  self.payload = payload
15
15
  end
@@ -30,7 +30,7 @@ module AppMap
30
30
 
31
31
  class SQLReturn < AppMap::Event::MethodReturnIgnoreValue
32
32
  def initialize(path, lineno, parent_id, elapsed)
33
- super AppMap::Event.next_id_counter, :return, SQLHandler, :call, path, lineno, false, Thread.current.object_id
33
+ super AppMap::Event.next_id_counter, :return, SQLHandler, :call, path, lineno, Thread.current.object_id
34
34
 
35
35
  self.parent_id = parent_id
36
36
  self.elapsed = elapsed
@@ -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