appmap 0.66.2 → 0.68.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.
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'appmap/event'
4
4
  require 'appmap/util'
5
+ require 'rack'
5
6
 
6
7
  module AppMap
7
8
  module Handler
@@ -60,10 +61,14 @@ module AppMap
60
61
  def initialize(response, parent_id, elapsed)
61
62
  super AppMap::Event.next_id_counter, :return, Thread.current.object_id
62
63
 
63
- self.status = response.code.to_i
64
+ if response
65
+ self.status = response.code.to_i
66
+ self.headers = NetHTTP.copy_headers(response)
67
+ else
68
+ self.headers = {}
69
+ end
64
70
  self.parent_id = parent_id
65
71
  self.elapsed = elapsed
66
- self.headers = NetHTTP.copy_headers(response)
67
72
  end
68
73
 
69
74
  def to_h
data/lib/appmap/hook.rb CHANGED
@@ -15,10 +15,23 @@ module AppMap
15
15
  @method_arity = ::Method.instance_method(:arity)
16
16
 
17
17
  class << self
18
- def lock_builtins
19
- return if @builtins_hooked
18
+ def hook_builtins?
19
+ Mutex.new.synchronize do
20
+ @hook_builtins = true if @hook_builtins.nil?
20
21
 
21
- @builtins_hooked = true
22
+ return false unless @hook_builtins
23
+
24
+ @hook_builtins = false
25
+ true
26
+ end
27
+ end
28
+
29
+ def already_hooked?(method)
30
+ # After a method is defined, the statement "module_function <the-method>" can convert that method
31
+ # into a module (class) method. The method is hooked first when it's defined, then AppMap will attempt to
32
+ # hook it again when it's redefined as a module method. So we check the method source location - if it's
33
+ # part of the AppMap source tree, we ignore it.
34
+ method.source_location && method.source_location[0].index(__dir__) == 0
22
35
  end
23
36
 
24
37
  # Return the class, separator ('.' or '#'), and method name for
@@ -79,42 +92,43 @@ module AppMap
79
92
  # hook_builtins builds hooks for code that is built in to the Ruby standard library.
80
93
  # No TracePoint events are emitted for builtins, so a separate hooking mechanism is needed.
81
94
  def hook_builtins
82
- return unless self.class.lock_builtins
95
+ return unless self.class.hook_builtins?
83
96
 
84
- class_from_string = lambda do |fq_class|
85
- fq_class.split('::').inject(Object) do |mod, class_name|
86
- mod.const_get(class_name)
87
- end
88
- end
97
+ hook_loaded_code = lambda do |hooks_by_class, builtin|
98
+ hooks_by_class.each do |class_name, hooks|
99
+ Array(hooks).each do |hook|
100
+ require hook.package.require_name if builtin && hook.package.require_name && hook.package.require_name != 'ruby'
89
101
 
90
- config.builtin_hooks.each do |class_name, hooks|
91
- Array(hooks).each do |hook|
92
- require hook.package.package_name if hook.package.package_name && hook.package.package_name != 'ruby'
93
- Array(hook.method_names).each do |method_name|
94
- method_name = method_name.to_sym
95
- base_cls = class_from_string.(class_name)
102
+ Array(hook.method_names).each do |method_name|
103
+ method_name = method_name.to_sym
104
+ base_cls = Util::class_from_string(class_name, must: false)
105
+ next unless base_cls
96
106
 
97
- hook_method = lambda do |entry|
98
- cls, method = entry
99
- return false if config.never_hook?(cls, method)
107
+ hook_method = lambda do |entry|
108
+ cls, method = entry
109
+ return false if config.never_hook?(cls, method)
100
110
 
101
- Hook::Method.new(hook.package, cls, method).activate
102
- end
111
+ Hook::Method.new(hook.package, cls, method).activate
112
+ end
103
113
 
104
- methods = []
105
- methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
106
- if base_cls.respond_to?(:singleton_class)
107
- methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
108
- end
109
- methods.compact!
110
- if methods.empty?
111
- warn "Method #{method_name} not found on #{base_cls.name}"
112
- else
113
- methods.each(&hook_method)
114
+ methods = []
115
+ methods << [ base_cls, base_cls.public_instance_method(method_name) ] rescue nil
116
+ if base_cls.respond_to?(:singleton_class)
117
+ methods << [ base_cls.singleton_class, base_cls.singleton_class.public_instance_method(method_name) ] rescue nil
118
+ end
119
+ methods.compact!
120
+ if methods.empty?
121
+ warn "Method #{method_name} not found on #{base_cls.name}" if LOG
122
+ else
123
+ methods.each(&hook_method)
124
+ end
114
125
  end
115
126
  end
116
127
  end
117
128
  end
129
+
130
+ hook_loaded_code.(config.builtin_hooks, true)
131
+ hook_loaded_code.(config.gem_hooks, false)
118
132
  end
119
133
 
120
134
  protected
@@ -165,6 +179,8 @@ module AppMap
165
179
  next
166
180
  end
167
181
 
182
+ next if self.class.already_hooked?(method)
183
+
168
184
  warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
169
185
 
170
186
  disasm = RubyVM::InstructionSequence.disasm(method)
@@ -142,6 +142,7 @@ if AppMap::Minitest.enabled?
142
142
  alias run_without_hook run
143
143
 
144
144
  def run
145
+ GC.start
145
146
  AppMap::Minitest.begin_test self, name
146
147
  begin
147
148
  run_without_hook
data/lib/appmap/rspec.rb CHANGED
@@ -87,6 +87,7 @@ module AppMap
87
87
  end
88
88
 
89
89
  warn "Starting recording of example #{example}@#{source_location}" if AppMap::RSpec::LOG
90
+ GC.start
90
91
  @trace = AppMap.tracing.trace
91
92
  @webdriver_port = webdriver_port.()
92
93
  end
data/lib/appmap/util.rb CHANGED
@@ -21,6 +21,14 @@ module AppMap
21
21
  WHITE = "\e[37m"
22
22
 
23
23
  class << self
24
+ def class_from_string(fq_class, must: true)
25
+ fq_class.split('::').inject(Object) do |mod, class_name|
26
+ mod.const_get(class_name)
27
+ end
28
+ rescue NameError
29
+ raise if must
30
+ end
31
+
24
32
  def parse_function_name(name)
25
33
  package_tokens = name.split('/')
26
34
 
@@ -3,7 +3,7 @@
3
3
  module AppMap
4
4
  URL = 'https://github.com/applandinc/appmap-ruby'
5
5
 
6
- VERSION = '0.66.2'
6
+ VERSION = '0.68.1'
7
7
 
8
8
  APPMAP_FORMAT_VERSION = '1.5.1'
9
9
 
data/lib/appmap.rb CHANGED
@@ -74,6 +74,6 @@ lambda do
74
74
  require 'appmap/depends'
75
75
  end
76
76
 
77
- end.call
77
+ end.call unless ENV['APPMAP_AUTOREQUIRE'] == 'false'
78
78
 
79
- AppMap.initialize_configuration if ENV['APPMAP'] == 'true'
79
+ AppMap.initialize_configuration if ENV['APPMAP'] == 'true' && ENV['APPMAP_INITIALIZE'] != 'false'
data/spec/config_spec.rb CHANGED
@@ -4,83 +4,243 @@ require 'rails_spec_helper'
4
4
  require 'appmap/config'
5
5
 
6
6
  describe AppMap::Config, docker: false do
7
- it 'loads from a Hash' do
7
+ it 'loads as expected' do
8
8
  config_data = {
9
- exclude: [],
10
9
  name: 'test',
11
- packages: [
10
+ packages: [],
11
+ functions: [
12
12
  {
13
- path: 'path-1'
13
+ name: 'pkg/cls#fn',
14
14
  },
15
15
  {
16
- path: 'path-2',
17
- exclude: [ 'exclude-1' ]
18
- }
19
- ],
20
- functions: [
21
- {
22
- package: 'pkg',
23
- class: 'cls',
24
- function: 'fn',
25
- label: 'lbl'
16
+ methods: ['cls#new_fn'],
17
+ path: 'pkg'
26
18
  }
27
19
  ]
28
20
  }.deep_stringify_keys!
29
21
  config = AppMap::Config.load(config_data)
30
22
 
31
- config_expectation = {
32
- exclude: [],
33
- name: 'test',
34
- packages: [
23
+ expect(JSON.parse(JSON.generate(config.as_json))).to eq(JSON.parse(<<~FIXTURE))
24
+ {
25
+ "name": "test",
26
+ "appmap_dir": "tmp/appmap",
27
+ "packages": [
28
+ ],
29
+ "swagger_config": {
30
+ "project_name": null,
31
+ "project_version": "1.0",
32
+ "output_dir": "swagger",
33
+ "description": "Generate Swagger from AppMaps"
34
+ },
35
+ "depends_config": {
36
+ "base_dir": null,
37
+ "base_branches": [
38
+ "remotes/origin/main",
39
+ "remotes/origin/master"
40
+ ],
41
+ "test_file_patterns": [
42
+ "spec/**/*_spec.rb",
43
+ "test/**/*_test.rb"
44
+ ],
45
+ "dependent_tasks": [
46
+ "swagger"
47
+ ],
48
+ "description": "Bring AppMaps up to date with local file modifications, and updated derived data such as Swagger files",
49
+ "rspec_environment_method": "AppMap::Depends.test_env",
50
+ "minitest_environment_method": "AppMap::Depends.test_env",
51
+ "rspec_select_tests_method": "AppMap::Depends.select_rspec_tests",
52
+ "minitest_select_tests_method": "AppMap::Depends.select_minitest_tests",
53
+ "rspec_test_command_method": "AppMap::Depends.rspec_test_command",
54
+ "minitest_test_command_method": "AppMap::Depends.minitest_test_command"
55
+ },
56
+ "hook_paths": [
57
+ "pkg",
58
+ "#{Gem.loaded_specs['activesupport'].gem_dir}"
59
+ ],
60
+ "exclude": [
61
+ ],
62
+ "functions": [
35
63
  {
36
- path: 'path-1',
37
- handler_class: 'AppMap::Handler::Function'
64
+ "cls": "cls",
65
+ "target_methods": {
66
+ "package": "pkg",
67
+ "method_names": [
68
+ "fn"
69
+ ]
70
+ }
38
71
  },
39
72
  {
40
- path: 'path-2',
41
- handler_class: 'AppMap::Handler::Function',
42
- exclude: [ 'exclude-1' ]
73
+ "cls": "cls",
74
+ "target_methods": {
75
+ "package": "pkg",
76
+ "method_names": [
77
+ "new_fn"
78
+ ]
79
+ }
43
80
  }
44
81
  ],
45
- functions: [
46
- {
47
- package: 'pkg',
48
- class: 'cls',
49
- functions: [ :fn ],
50
- labels: ['lbl']
51
- }
52
- ]
53
- }.deep_stringify_keys!
54
-
55
- expect(config.to_h.deep_stringify_keys!).to eq(config_expectation)
56
- end
57
-
58
- it 'interprets a function in canonical name format' do
59
- config_data = {
60
- name: 'test',
61
- packages: [],
62
- functions: [
63
- {
64
- name: 'pkg/cls#fn',
65
- }
66
- ]
67
- }.deep_stringify_keys!
68
- config = AppMap::Config.load(config_data)
69
-
70
- config_expectation = {
71
- exclude: [],
72
- name: 'test',
73
- packages: [],
74
- functions: [
75
- {
76
- package: 'pkg',
77
- class: 'cls',
78
- functions: [ :fn ],
79
- }
80
- ]
81
- }.deep_stringify_keys!
82
-
83
- expect(config.to_h.deep_stringify_keys!).to eq(config_expectation)
82
+ "builtin_hooks": {
83
+ "JSON::Ext::Parser": [
84
+ {
85
+ "package": "json",
86
+ "method_names": [
87
+ "parse"
88
+ ]
89
+ }
90
+ ],
91
+ "JSON::Ext::Generator::State": [
92
+ {
93
+ "package": "json",
94
+ "method_names": [
95
+ "generate"
96
+ ]
97
+ }
98
+ ],
99
+ "Net::HTTP": [
100
+ {
101
+ "package": "net/http",
102
+ "method_names": [
103
+ "request"
104
+ ]
105
+ }
106
+ ],
107
+ "OpenSSL::PKey::PKey": [
108
+ {
109
+ "package": "openssl",
110
+ "method_names": [
111
+ "sign"
112
+ ]
113
+ }
114
+ ],
115
+ "OpenSSL::X509::Request": [
116
+ {
117
+ "package": "openssl",
118
+ "method_names": [
119
+ "sign"
120
+ ]
121
+ },
122
+ {
123
+ "package": "openssl",
124
+ "method_names": [
125
+ "verify"
126
+ ]
127
+ }
128
+ ],
129
+ "OpenSSL::X509::Certificate": [
130
+ {
131
+ "package": "openssl",
132
+ "method_names": [
133
+ "sign"
134
+ ]
135
+ }
136
+ ],
137
+ "OpenSSL::PKCS5": [
138
+ {
139
+ "package": "openssl",
140
+ "method_names": [
141
+ "pbkdf2_hmac"
142
+ ]
143
+ },
144
+ {
145
+ "package": "openssl",
146
+ "method_names": [
147
+ "pbkdf2_hmac_sha1"
148
+ ]
149
+ }
150
+ ],
151
+ "OpenSSL::Cipher": [
152
+ {
153
+ "package": "openssl",
154
+ "method_names": [
155
+ "encrypt"
156
+ ]
157
+ },
158
+ {
159
+ "package": "openssl",
160
+ "method_names": [
161
+ "decrypt"
162
+ ]
163
+ }
164
+ ],
165
+ "Psych": [
166
+ {
167
+ "package": "yaml",
168
+ "method_names": [
169
+ "load"
170
+ ]
171
+ },
172
+ {
173
+ "package": "yaml",
174
+ "method_names": [
175
+ "load_stream"
176
+ ]
177
+ },
178
+ {
179
+ "package": "yaml",
180
+ "method_names": [
181
+ "parse"
182
+ ]
183
+ },
184
+ {
185
+ "package": "yaml",
186
+ "method_names": [
187
+ "parse_stream"
188
+ ]
189
+ },
190
+ {
191
+ "package": "yaml",
192
+ "method_names": [
193
+ "dump"
194
+ ]
195
+ },
196
+ {
197
+ "package": "yaml",
198
+ "method_names": [
199
+ "dump_stream"
200
+ ]
201
+ }
202
+ ]
203
+ },
204
+ "gem_hooks": {
205
+ "cls": [
206
+ {
207
+ "package": "pkg",
208
+ "method_names": [
209
+ "fn"
210
+ ]
211
+ },
212
+ {
213
+ "package": "pkg",
214
+ "method_names": [
215
+ "new_fn"
216
+ ]
217
+ }
218
+ ],
219
+ "ActiveSupport::Callbacks::CallbackSequence": [
220
+ {
221
+ "package": "activesupport",
222
+ "method_names": [
223
+ "invoke_before"
224
+ ]
225
+ },
226
+ {
227
+ "package": "activesupport",
228
+ "method_names": [
229
+ "invoke_after"
230
+ ]
231
+ }
232
+ ],
233
+ "ActiveSupport::SecurityUtils": [
234
+ {
235
+ "package": "activesupport",
236
+ "method_names": [
237
+ "secure_compare"
238
+ ]
239
+ }
240
+ ]
241
+ }
242
+ }
243
+ FIXTURE
84
244
  end
85
245
 
86
246
  context do
@@ -94,7 +254,8 @@ describe AppMap::Config, docker: false do
94
254
  expect(config.to_h).to eq(YAML.load(<<~CONFIG))
95
255
  :name: appmap-ruby
96
256
  :packages:
97
- - :path: lib
257
+ - :name: lib
258
+ :path: lib
98
259
  :handler_class: AppMap::Handler::Function
99
260
  :shallow: false
100
261
  :functions: []
@@ -104,7 +104,7 @@ describe 'Depends API' do
104
104
  describe '.run_tests' do
105
105
  def run_tests
106
106
  Dir.chdir 'spec/fixtures/depends' do
107
- api.run_tests([ 'spec/actual_rspec_test.rb', 'test/actual_minitest_test.rb' ], appmap_dir: Pathname.new(DEPENDS_TEST_DIR).expand_path.to_s)
107
+ api.run_tests([ 'spec/actual_rspec_test.rb', 'test/actual_minitest_test.rb' ], appmap_dir: Pathname.new('.').expand_path.to_s)
108
108
  end
109
109
  end
110
110
 
@@ -5,9 +5,14 @@ require 'active_support'
5
5
  require 'active_support/core_ext'
6
6
  require 'open3'
7
7
 
8
+ # docker compose v2 replaced the --filter flag with --status
9
+ PS_CMD=`docker-compose --version` =~ /version v2/ ?
10
+ "docker-compose ps -q --status running" :
11
+ "docker-compose ps -q --filter health=healthy"
12
+
8
13
  def wait_for_container(app_name)
9
14
  start_time = Time.now
10
- until `docker-compose ps -q --filter health=healthy #{app_name}`.strip != ''
15
+ until `#{PS_CMD} #{app_name}`.strip != ''
11
16
  elapsed = Time.now - start_time
12
17
  raise "Timeout waiting for container #{app_name} to be ready" if elapsed > 10
13
18
 
@@ -2,28 +2,40 @@
2
2
 
3
3
  require 'test_helper'
4
4
 
5
+ schema_path = File.expand_path('../../config-schema.yml', __FILE__)
6
+ CONFIG_SCHEMA = YAML.safe_load(File.read(schema_path))
5
7
  class AgentSetupValidateTest < Minitest::Test
6
8
  NON_EXISTING_CONFIG_FILENAME = '123.yml'
7
9
  INVALID_YAML_CONFIG_FILENAME = 'spec/fixtures/config/invalid_yaml_config.yml'
8
10
  INVALID_CONFIG_FILENAME = 'spec/fixtures/config/invalid_config.yml'
9
11
  MISSING_PATH_OR_GEM_CONFIG_FILENAME = 'spec/fixtures/config/missing_path_or_gem.yml'
10
12
 
13
+ def check_output(output, expected_errors)
14
+ expected = JSON.pretty_generate(
15
+ {
16
+ version: 2,
17
+ errors: expected_errors,
18
+ schema: CONFIG_SCHEMA
19
+ }
20
+ )
21
+ assert_equal(expected, output.strip)
22
+ end
23
+
11
24
  def test_init_when_config_exists
12
25
  output = `./exe/appmap-agent-validate`
13
26
  assert_equal 0, $CHILD_STATUS.exitstatus
14
- expected = JSON.pretty_generate([
27
+ check_output(output, [
15
28
  {
16
29
  level: :error,
17
30
  message: 'AppMap auto-configuration is currently not available for non Rails projects'
18
31
  }
19
32
  ])
20
- assert_equal expected, output.strip
21
33
  end
22
34
 
23
35
  def test_init_with_non_existing_config_file
24
36
  output = `./exe/appmap-agent-validate -c #{NON_EXISTING_CONFIG_FILENAME}`
25
37
  assert_equal 0, $CHILD_STATUS.exitstatus
26
- expected = JSON.pretty_generate([
38
+ check_output(output, [
27
39
  {
28
40
  level: :error,
29
41
  message: 'AppMap auto-configuration is currently not available for non Rails projects'
@@ -34,13 +46,12 @@ class AgentSetupValidateTest < Minitest::Test
34
46
  message: "AppMap configuration #{NON_EXISTING_CONFIG_FILENAME} file does not exist"
35
47
  }
36
48
  ])
37
- assert_equal expected, output.strip
38
49
  end
39
50
 
40
51
  def test_init_with_invalid_YAML
41
52
  output = `./exe/appmap-agent-validate -c #{INVALID_YAML_CONFIG_FILENAME}`
42
53
  assert_equal 0, $CHILD_STATUS.exitstatus
43
- expected = JSON.pretty_generate([
54
+ check_output(output, [
44
55
  {
45
56
  level: :error,
46
57
  message: 'AppMap auto-configuration is currently not available for non Rails projects'
@@ -53,13 +64,12 @@ class AgentSetupValidateTest < Minitest::Test
53
64
  'did not find expected key while parsing a block mapping at line 1 column 1'
54
65
  }
55
66
  ])
56
- assert_equal expected, output.strip
57
67
  end
58
68
 
59
69
  def test_init_with_invalid_data_config
60
70
  output = `./exe/appmap-agent-validate -c #{INVALID_CONFIG_FILENAME}`
61
71
  assert_equal 0, $CHILD_STATUS.exitstatus
62
- expected = JSON.pretty_generate([
72
+ check_output(output, [
63
73
  {
64
74
  level: :error,
65
75
  message: 'AppMap auto-configuration is currently not available for non Rails projects'
@@ -71,13 +81,12 @@ class AgentSetupValidateTest < Minitest::Test
71
81
  detailed_message: "no implicit conversion of String into Integer"
72
82
  }
73
83
  ])
74
- assert_equal expected, output.strip
75
84
  end
76
85
 
77
86
  def test_init_with_missing_package_key
78
87
  output = `./exe/appmap-agent-validate -c #{MISSING_PATH_OR_GEM_CONFIG_FILENAME}`
79
88
  assert_equal 0, $CHILD_STATUS.exitstatus
80
- expected = JSON.pretty_generate([
89
+ check_output(output, [
81
90
  {
82
91
  level: :error,
83
92
  message: 'AppMap auto-configuration is currently not available for non Rails projects'
@@ -89,6 +98,5 @@ class AgentSetupValidateTest < Minitest::Test
89
98
  detailed_message: "AppMap config 'package' element should specify 'gem' or 'path'"
90
99
  }
91
100
  ])
92
- assert_equal expected, output.strip
93
101
  end
94
102
  end
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'appmap', git: 'applandinc/appmap-ruby', branch: `git rev-parse --abbrev-ref HEAD`.strip
4
+ gem 'minitest'
5
+ gem 'mocha'
@@ -0,0 +1,5 @@
1
+ name: mocha_mock_app
2
+ packages:
3
+ - path: lib
4
+ - gem: mocha
5
+ shallow: false
@@ -0,0 +1,5 @@
1
+ class Sheep
2
+ def baa
3
+ 'baa'
4
+ end
5
+ end
@@ -0,0 +1,18 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __dir__)
2
+
3
+ require 'minitest/autorun'
4
+
5
+ require 'appmap'
6
+ require 'appmap/minitest' if ENV['APPMAP_AUTOREQUIRE'] == 'false'
7
+
8
+ require 'sheep'
9
+ require 'mocha/minitest'
10
+
11
+ class SheepTest < Minitest::Test
12
+ def test_sheep
13
+ sheep = mock('sheep')
14
+ sheep.responds_like(Sheep.new)
15
+ sheep.expects(:baa).returns('baa')
16
+ sheep.baa
17
+ end
18
+ end