appmap 0.25.2 → 0.28.1

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