appmap 0.66.2 → 0.68.1

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