appmap 0.27.0 → 0.33.0

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