appmap 0.27.0 → 0.33.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -3
  3. data/CHANGELOG.md +37 -0
  4. data/README.md +170 -29
  5. data/Rakefile +1 -1
  6. data/appmap.gemspec +1 -0
  7. data/exe/appmap +3 -1
  8. data/lib/appmap.rb +54 -38
  9. data/lib/appmap/algorithm/stats.rb +2 -1
  10. data/lib/appmap/class_map.rb +21 -28
  11. data/lib/appmap/command/record.rb +2 -61
  12. data/lib/appmap/config.rb +89 -0
  13. data/lib/appmap/cucumber.rb +89 -0
  14. data/lib/appmap/event.rb +28 -19
  15. data/lib/appmap/hook.rb +56 -128
  16. data/lib/appmap/hook/method.rb +78 -0
  17. data/lib/appmap/metadata.rb +62 -0
  18. data/lib/appmap/middleware/remote_recording.rb +2 -6
  19. data/lib/appmap/minitest.rb +141 -0
  20. data/lib/appmap/open.rb +57 -0
  21. data/lib/appmap/rails/action_handler.rb +7 -7
  22. data/lib/appmap/rails/sql_handler.rb +10 -8
  23. data/lib/appmap/railtie.rb +2 -2
  24. data/lib/appmap/record.rb +27 -0
  25. data/lib/appmap/rspec.rb +9 -37
  26. data/lib/appmap/trace.rb +18 -10
  27. data/lib/appmap/util.rb +59 -0
  28. data/lib/appmap/version.rb +1 -1
  29. data/package-lock.json +3 -3
  30. data/spec/abstract_controller4_base_spec.rb +1 -1
  31. data/spec/abstract_controller_base_spec.rb +9 -2
  32. data/spec/config_spec.rb +3 -3
  33. data/spec/fixtures/hook/compare.rb +7 -0
  34. data/spec/fixtures/hook/singleton_method.rb +54 -0
  35. data/spec/fixtures/rails_users_app/Gemfile +1 -0
  36. data/spec/fixtures/rails_users_app/features/api_users.feature +13 -0
  37. data/spec/fixtures/rails_users_app/features/support/env.rb +4 -0
  38. data/spec/fixtures/rails_users_app/features/support/hooks.rb +11 -0
  39. data/spec/fixtures/rails_users_app/features/support/steps.rb +18 -0
  40. data/spec/hook_spec.rb +228 -53
  41. data/spec/open_spec.rb +19 -0
  42. data/spec/rails_spec_helper.rb +2 -0
  43. data/spec/record_sql_rails_pg_spec.rb +56 -33
  44. data/spec/rspec_feature_metadata_spec.rb +2 -0
  45. data/spec/spec_helper.rb +4 -0
  46. data/spec/util_spec.rb +21 -0
  47. data/test/cli_test.rb +4 -4
  48. data/test/cucumber_test.rb +72 -0
  49. data/test/fixtures/cucumber4_recorder/Gemfile +5 -0
  50. data/test/fixtures/cucumber4_recorder/appmap.yml +3 -0
  51. data/test/fixtures/cucumber4_recorder/features/say_hello.feature +5 -0
  52. data/test/fixtures/cucumber4_recorder/features/support/env.rb +5 -0
  53. data/test/fixtures/cucumber4_recorder/features/support/hooks.rb +11 -0
  54. data/test/fixtures/cucumber4_recorder/features/support/steps.rb +9 -0
  55. data/test/fixtures/cucumber4_recorder/lib/hello.rb +7 -0
  56. data/test/fixtures/cucumber_recorder/Gemfile +5 -0
  57. data/test/fixtures/cucumber_recorder/appmap.yml +3 -0
  58. data/test/fixtures/cucumber_recorder/features/say_hello.feature +5 -0
  59. data/test/fixtures/cucumber_recorder/features/support/env.rb +5 -0
  60. data/test/fixtures/cucumber_recorder/features/support/hooks.rb +11 -0
  61. data/test/fixtures/cucumber_recorder/features/support/steps.rb +9 -0
  62. data/test/fixtures/cucumber_recorder/lib/hello.rb +7 -0
  63. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  64. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  65. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  66. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  67. data/test/fixtures/process_recorder/appmap.yml +3 -0
  68. data/test/fixtures/process_recorder/hello.rb +9 -0
  69. data/test/minitest_test.rb +38 -0
  70. data/test/record_process_test.rb +35 -0
  71. data/test/test_helper.rb +1 -0
  72. metadata +55 -3
  73. 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)
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext'
4
-
5
3
  module AppMap
6
4
  class ClassMap
7
5
  module HasChildren
@@ -50,7 +48,7 @@ module AppMap
50
48
  end
51
49
  end
52
50
  Function = Struct.new(:name) do
53
- attr_accessor :static, :location
51
+ attr_accessor :static, :location, :labels
54
52
 
55
53
  def type
56
54
  'function'
@@ -61,8 +59,9 @@ module AppMap
61
59
  name: name,
62
60
  type: type,
63
61
  location: location,
64
- static: static
65
- }
62
+ static: static,
63
+ labels: labels
64
+ }.delete_if {|k,v| v.nil?}
66
65
  end
67
66
  end
68
67
  end
@@ -71,35 +70,21 @@ module AppMap
71
70
  def build_from_methods(config, methods)
72
71
  root = Types::Root.new
73
72
  methods.each do |method|
74
- package = package_for_method(config.packages, method)
75
- add_function root, package.path, method
73
+ package = config.package_for_method(method) \
74
+ or raise "No package found for method #{method}"
75
+ add_function root, package, method
76
76
  end
77
77
  root.children.map(&:to_h)
78
78
  end
79
79
 
80
80
  protected
81
81
 
82
- def package_for_method(packages, method)
83
- location = method.method.source_location
84
- location_file, = location
85
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
86
-
87
- packages.find do |pkg|
88
- (location_file.index(pkg.path) == 0) &&
89
- !pkg.exclude.find { |p| location_file.index(p) }
90
- end or raise "No package found for method #{method}"
91
- end
92
-
93
- def add_function(root, package_name, method)
94
- location = method.method.source_location
95
- location_file, lineno = location
96
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
97
-
98
- static = method.method.owner.singleton_class?
82
+ def add_function(root, package, method)
83
+ static = method.static
99
84
 
100
85
  object_infos = [
101
86
  {
102
- name: package_name,
87
+ name: package.path,
103
88
  type: 'package'
104
89
  }
105
90
  ]
@@ -109,12 +94,20 @@ module AppMap
109
94
  type: 'class'
110
95
  }
111
96
  end
112
- object_infos << {
113
- name: method.method.name,
97
+ function_info = {
98
+ name: method.name,
114
99
  type: 'function',
115
- location: [ location_file, lineno ].join(':'),
116
100
  static: static
117
101
  }
102
+ location = method.source_location
103
+ if location
104
+ location_file, lineno = location
105
+ location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
106
+ function_info[:location] = [ location_file, lineno ].join(':')
107
+ end
108
+ function_info[:labels] = package.labels if package.labels
109
+ object_infos << function_info
110
+
118
111
  parent = root
119
112
  object_infos.each do |info|
120
113
  parent = find_or_create parent.children, info do
@@ -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
+ module AppMap
4
+ Package = Struct.new(:path, :package_name, :exclude, :labels) do
5
+ def initialize(path, package_name, exclude, labels = nil)
6
+ super
7
+ end
8
+
9
+ def to_h
10
+ {
11
+ path: path,
12
+ package_name: package_name,
13
+ exclude: exclude.blank? ? nil : exclude,
14
+ labels: labels.blank? ? nil : labels
15
+ }.compact
16
+ end
17
+ end
18
+
19
+ class Config
20
+ # Methods that should always be hooked, with their containing
21
+ # package and labels that should be applied to them.
22
+ HOOKED_METHODS = {
23
+ 'ActiveSupport::SecurityUtils' => {
24
+ secure_compare: Package.new('active_support', nil, nil, ['security'])
25
+ }
26
+ }
27
+
28
+ attr_reader :name, :packages
29
+ def initialize(name, packages = [])
30
+ @name = name
31
+ @packages = packages
32
+ end
33
+
34
+ class << self
35
+ # Loads configuration data from a file, specified by the file name.
36
+ def load_from_file(config_file_name)
37
+ require 'yaml'
38
+ load YAML.safe_load(::File.read(config_file_name))
39
+ end
40
+
41
+ # Loads configuration from a Hash.
42
+ def load(config_data)
43
+ packages = (config_data['packages'] || []).map do |package|
44
+ Package.new(package['path'], nil, package['exclude'] || [])
45
+ end
46
+ Config.new config_data['name'], packages
47
+ end
48
+ end
49
+
50
+ def to_h
51
+ {
52
+ name: name,
53
+ packages: packages.map(&:to_h)
54
+ }
55
+ end
56
+
57
+ def package_for_method(method)
58
+ defined_class, _, method_name = Hook.qualify_method_name(method)
59
+ hooked_method = find_hooked_method(defined_class, method_name)
60
+ return hooked_method if hooked_method
61
+
62
+ location = method.source_location
63
+ location_file, = location
64
+ return unless location_file
65
+
66
+ location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
67
+ packages.find do |pkg|
68
+ (location_file.index(pkg.path) == 0) &&
69
+ !pkg.exclude.find { |p| location_file.index(p) }
70
+ end
71
+ end
72
+
73
+ def included_by_location?(method)
74
+ !!package_for_method(method)
75
+ end
76
+
77
+ def always_hook?(defined_class, method_name)
78
+ !!find_hooked_method(defined_class, method_name)
79
+ end
80
+
81
+ def find_hooked_method(defined_class, method_name)
82
+ find_hooked_class(defined_class)[method_name]
83
+ end
84
+
85
+ def find_hooked_class(defined_class)
86
+ HOOKED_METHODS[defined_class] || {}
87
+ end
88
+ end
89
+ end
@@ -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,24 +15,15 @@ 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, :thread_id)
19
19
 
20
20
  class MethodEvent < MethodEventStruct
21
21
  LIMIT = 100
22
22
 
23
23
  class << self
24
- def build_from_invocation(me, event_type, defined_class, method)
25
- singleton = method.owner.singleton_class?
26
-
24
+ def build_from_invocation(me, event_type)
27
25
  me.id = AppMap::Event.next_id_counter
28
26
  me.event = event_type
29
- me.defined_class = defined_class
30
- me.method_id = method.name.to_s
31
- path = method.source_location[0]
32
- path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
33
- me.path = path
34
- me.lineno = method.source_location[1]
35
- me.static = singleton
36
27
  me.thread_id = Thread.current.object_id
37
28
  end
38
29
 
@@ -61,16 +52,25 @@ module AppMap
61
52
  (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
62
53
  end
63
54
  end
64
-
65
- alias static? static
66
55
  end
67
56
 
68
57
  class MethodCall < MethodEvent
69
- attr_accessor :parameters, :receiver
58
+ attr_accessor :defined_class, :method_id, :path, :lineno, :parameters, :receiver, :static
70
59
 
71
60
  class << self
72
61
  def build_from_invocation(mc = MethodCall.new, defined_class, method, receiver, arguments)
73
62
  mc.tap do
63
+ static = receiver.is_a?(Module)
64
+ mc.defined_class = defined_class
65
+ mc.method_id = method.name.to_s
66
+ if method.source_location
67
+ path = method.source_location[0]
68
+ path = path[Dir.pwd.length + 1..-1] if path.index(Dir.pwd) == 0
69
+ mc.path = path
70
+ mc.lineno = method.source_location[1]
71
+ else
72
+ mc.path = [ defined_class, static ? '.' : '#', method.name ].join
73
+ end
74
74
  mc.parameters = method.parameters.map.with_index do |method_param, idx|
75
75
  param_type, param_name = method_param
76
76
  param_name ||= 'arg'
@@ -88,28 +88,37 @@ module AppMap
88
88
  object_id: receiver.__id__,
89
89
  value: display_string(receiver)
90
90
  }
91
- MethodEvent.build_from_invocation(mc, :call, defined_class, method)
91
+ mc.static = static
92
+ MethodEvent.build_from_invocation(mc, :call)
92
93
  end
93
94
  end
94
95
  end
95
96
 
96
97
  def to_h
97
98
  super.tap do |h|
99
+ h[:defined_class] = defined_class
100
+ h[:method_id] = method_id
101
+ h[:path] = path
102
+ h[:lineno] = lineno
103
+ h[:static] = static
98
104
  h[:parameters] = parameters
99
105
  h[:receiver] = receiver
106
+ h.delete_if { |_, v| v.nil? }
100
107
  end
101
108
  end
109
+
110
+ alias static? static
102
111
  end
103
112
 
104
113
  class MethodReturnIgnoreValue < MethodEvent
105
114
  attr_accessor :parent_id, :elapsed
106
115
 
107
116
  class << self
108
- def build_from_invocation(mr = MethodReturnIgnoreValue.new, defined_class, method, parent_id, elapsed)
117
+ def build_from_invocation(mr = MethodReturnIgnoreValue.new, parent_id, elapsed)
109
118
  mr.tap do |_|
110
119
  mr.parent_id = parent_id
111
120
  mr.elapsed = elapsed
112
- MethodEvent.build_from_invocation(mr, :return, defined_class, method)
121
+ MethodEvent.build_from_invocation(mr, :return)
113
122
  end
114
123
  end
115
124
  end
@@ -126,7 +135,7 @@ module AppMap
126
135
  attr_accessor :return_value, :exceptions
127
136
 
128
137
  class << self
129
- def build_from_invocation(mr = MethodReturn.new, defined_class, method, parent_id, elapsed, return_value, exception)
138
+ def build_from_invocation(mr = MethodReturn.new, parent_id, elapsed, return_value, exception)
130
139
  mr.tap do |_|
131
140
  if return_value
132
141
  mr.return_value = {
@@ -152,7 +161,7 @@ module AppMap
152
161
 
153
162
  mr.exceptions = exceptions
154
163
  end
155
- MethodReturnIgnoreValue.build_from_invocation(mr, defined_class, method, parent_id, elapsed)
164
+ MethodReturnIgnoreValue.build_from_invocation(mr, parent_id, elapsed)
156
165
  end
157
166
  end
158
167
  end