appmap 0.33.0 → 0.34.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae23c62b9a3f01cc3b7680706746d2f51c94c3a776978a8ee2d4e0e8501fca01
4
- data.tar.gz: 4aa33af19ed0c31b826f8cffa75e147e2781fe93ed39353ace29cede8a7882b9
3
+ metadata.gz: 3b4318e1ef7e025a8f616022cefd323784e7b8ee8ccf06b1b7167b9937e860df
4
+ data.tar.gz: c6a743f2c7f4ebe8cd58f7ae971b5dae15791bc808dab9d4dcf87f4971986818
5
5
  SHA512:
6
- metadata.gz: 04a3c1bee70c02c96c1b71d5153a6ed44d668d6fc69d6da77cd6201364ede609bee93604f4ace391ddb38f8259841d6b334fdbe9051f1667d41cf3383281fe58
7
- data.tar.gz: c60d868b2c1fa9c1e8742856dd7f946f5bf1284cab01a4478708c1582add5ca8fd16002e150d8a2cd897819edb661a9082cb01ec3e9879686d85d49b341d4ba8
6
+ metadata.gz: 9f271a71bb8bffa7004d054ae91646c9c93a476b15cbe80c459a9cafcb13ab0169be6c8a2db9b8cc993c3bc767deef4c9c0ca4befe6bd6881cf2d93c7b692b64
7
+ data.tar.gz: 3b6c5067a325fa2a0aed71b5ed92179196061cd6cfa015f907a25dab798e757c2cd53efbd028513df1f22cc2e2b8aa9345f8d19aa27018eccfbee4cd39766ece
data/.gitignore CHANGED
@@ -14,4 +14,4 @@ Gemfile.lock
14
14
  appmap.json
15
15
  .vscode
16
16
  .byebug_history
17
-
17
+ /lib/appmap/appmap.bundle
@@ -0,0 +1 @@
1
+ appmap-ruby
@@ -1,3 +1,30 @@
1
+ # v0.34.5
2
+ * Ensure that hooking a method doesn't change its arity.
3
+
4
+ # v0.34.4
5
+ * Make sure `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.server_version` only calls
6
+ `ActiveRecord::Base.connection.database_version` if it's available.
7
+ * Fix `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.database_type` returns `:postgres`
8
+ in all supported versions of Rails.
9
+
10
+ # v0.34.3
11
+ * Fix a crash in `singleton_method_owner_name` that occurred if `__attached__.class` returned
12
+ something other than a `Module` or a `Class`.
13
+
14
+ # v0.34.2
15
+ * Add an extension that gets the name of the owner of a singleton method without calling
16
+ any methods that may have been redefined (e.g. `#to_s` or `.name`).
17
+
18
+ # v0.34.1
19
+ * Ensure that capturing events doesn't change the behavior of a hooked method that uses
20
+ `Time.now`. For example, if a test expects that `Time.now` will be called a certain
21
+ number of times by a hooked method, that expectation will now be met.
22
+ * Make sure `appmap/cucumber` requires `appmap`.
23
+
24
+ # v0.34.0
25
+
26
+ * Records builtin security and I/O methods from `OpenSSL`, `Net`, and `IO`.
27
+
1
28
  # v0.33.0
2
29
 
3
30
  * Added command `AppMap.open` to open an AppMap in the browser.
data/README.md CHANGED
@@ -57,6 +57,15 @@ end
57
57
 
58
58
  Then install with `bundle`.
59
59
 
60
+ **Railtie**
61
+
62
+ If you are using Ruby on Rails, require the railtie after Rails is loaded.
63
+
64
+ ```
65
+ # application.rb is a good place to do this, along with all the other railties.
66
+ require 'appmap/railtie'
67
+ ```
68
+
60
69
  # Configuration
61
70
 
62
71
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -267,7 +276,13 @@ $ bundle config local.appmap $(pwd)
267
276
  Run the tests via `rake`:
268
277
  ```
269
278
  $ bundle exec rake test
270
- ```
279
+ ```
280
+
281
+ The `test` target will build the native extension first, then run the tests. If you need
282
+ to build the extension separately, run
283
+ ```
284
+ $ bundle exec rake compile
285
+ ```
271
286
 
272
287
  ## Using fixture apps
273
288
 
@@ -286,7 +301,7 @@ resources such as a PostgreSQL database.
286
301
  To build the fixture container images, first run:
287
302
 
288
303
  ```sh-session
289
- $ bundle exec rake fixtures:all
304
+ $ bundle exec rake build:fixtures:all
290
305
  ```
291
306
 
292
307
  This will build the `appmap.gem`, along with a Docker image for each fixture app.
data/Rakefile CHANGED
@@ -6,6 +6,13 @@ require 'rdoc/task'
6
6
 
7
7
  require 'open3'
8
8
 
9
+ require "rake/extensiontask"
10
+
11
+ desc 'build the native extension'
12
+ Rake::ExtensionTask.new("appmap") do |ext|
13
+ ext.lib_dir = "lib/appmap"
14
+ end
15
+
9
16
  namespace 'gem' do
10
17
  require 'bundler/gem_tasks'
11
18
  end
@@ -104,7 +111,7 @@ end
104
111
  namespace :spec do
105
112
  RUBY_VERSIONS.each do |ruby_version|
106
113
  desc ruby_version
107
- task ruby_version, [:specs] => ["build:fixtures:#{ruby_version}:all"] do |_, task_args|
114
+ task ruby_version, [:specs] => ["compile", "build:fixtures:#{ruby_version}:all"] do |_, task_args|
108
115
  run_specs(ruby_version, task_args)
109
116
  end.tap do|t|
110
117
  desc "Run all specs"
@@ -119,13 +126,13 @@ Rake::RDocTask.new do |rd|
119
126
  rd.title = 'AppMap'
120
127
  end
121
128
 
122
- Rake::TestTask.new(:minitest) do |t|
129
+ Rake::TestTask.new(minitest: 'compile') do |t|
123
130
  t.libs << 'test'
124
131
  t.libs << 'lib'
125
132
  t.test_files = FileList['test/*_test.rb']
126
133
  end
127
134
 
128
- task spec: "spec:all"
135
+ task spec: %i[spec:all]
129
136
 
130
137
  task test: %i[spec:all minitest]
131
138
 
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
18
18
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
19
  spec.files = `git ls-files --no-deleted`.split("
20
20
  ")
21
+ spec.extensions << "ext/appmap/extconf.rb"
22
+
21
23
  spec.bindir = 'exe'
22
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
25
  spec.require_paths = ['lib']
@@ -34,6 +36,7 @@ Gem::Specification.new do |spec|
34
36
  spec.add_development_dependency 'rake', '>= 12.3.3'
35
37
  spec.add_development_dependency 'rdoc'
36
38
  spec.add_development_dependency 'rubocop'
39
+ spec.add_development_dependency "rake-compiler"
37
40
 
38
41
  # Testing
39
42
  spec.add_development_dependency 'climate_control'
@@ -42,4 +45,6 @@ Gem::Specification.new do |spec|
42
45
  spec.add_development_dependency 'rspec'
43
46
  spec.add_development_dependency 'selenium-webdriver'
44
47
  spec.add_development_dependency 'webdrivers', '~> 4.0'
48
+ spec.add_development_dependency 'timecop'
49
+ spec.add_development_dependency 'hashie'
45
50
  end
@@ -0,0 +1,95 @@
1
+ #include <ruby.h>
2
+ #include <ruby/intern.h>
3
+
4
+ // Seems like CLASS_OR_MODULE_P should really be in a header file in
5
+ // the ruby source -- it's in object.c and duplicated in eval.c. In
6
+ // the future, we'll fail if it does get moved to a header.
7
+ #define CLASS_OR_MODULE_P(obj) \
8
+ (!SPECIAL_CONST_P(obj) && \
9
+ (BUILTIN_TYPE(obj) == T_CLASS || BUILTIN_TYPE(obj) == T_MODULE))
10
+
11
+ #define ARITIES_KEY "__arities__"
12
+
13
+ VALUE am_AppMapHook;
14
+
15
+ static VALUE
16
+ singleton_method_owner_name(VALUE klass, VALUE method)
17
+ {
18
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
19
+ VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
20
+ if (!CLASS_OR_MODULE_P(attached)) {
21
+ attached = rb_funcall(attached, rb_intern("class"), 0);
22
+ }
23
+
24
+ // Did __attached__.class return an object that's a Module or a
25
+ // Class?
26
+ if (CLASS_OR_MODULE_P(attached)) {
27
+ // Yup, get it's name
28
+ return rb_mod_name(attached);
29
+ }
30
+
31
+ // Nope (which seems weird, but whatever). Fall back to calling
32
+ // #to_s on the method's owner and hope for the best.
33
+ return rb_funcall(owner, rb_intern("to_s"), 0);
34
+ }
35
+
36
+
37
+ static VALUE
38
+ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
39
+ {
40
+ VALUE arities_key = rb_intern(ARITIES_KEY);
41
+ VALUE arities = rb_ivar_get(mod, arities_key);
42
+
43
+ if (arities == Qundef || NIL_P(arities)) {
44
+ arities = rb_hash_new();
45
+ rb_ivar_set(mod, arities_key, arities);
46
+ }
47
+ rb_hash_aset(arities, name, arity);
48
+
49
+ return rb_funcall(mod, rb_intern("define_method"), 2, name, proc);
50
+ }
51
+
52
+ static VALUE
53
+ am_get_method_arity(VALUE method, VALUE orig_arity_method)
54
+ {
55
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
56
+ VALUE arities = rb_ivar_get(owner, rb_intern(ARITIES_KEY));
57
+ VALUE name = rb_funcall(method, rb_intern("name"), 0);
58
+ VALUE arity = Qnil;
59
+ // See if we saved an arity for the method.
60
+ if (!NIL_P(arities)) {
61
+ arity = rb_hash_aref(arities, name);
62
+ }
63
+ // Didn't find one, call the original method.
64
+ if (NIL_P(arity)) {
65
+ VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
+ arity = rb_funcall(bound_method, rb_intern("call"), 0);
67
+ }
68
+
69
+ return arity;
70
+ }
71
+
72
+ static VALUE
73
+ am_unbound_method_arity(VALUE method)
74
+ {
75
+ VALUE orig_unbound_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@unbound_method_arity"));
76
+ return am_get_method_arity(method, orig_unbound_method_arity);
77
+ }
78
+
79
+ static VALUE
80
+ am_method_arity(VALUE method)
81
+ {
82
+ VALUE orig_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@method_arity"));
83
+ return am_get_method_arity(method, orig_method_arity);
84
+ }
85
+
86
+ void Init_appmap() {
87
+ VALUE appmap = rb_define_module("AppMap");
88
+ am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
89
+
90
+ rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
91
+
92
+ rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
93
+ rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
94
+ rb_define_method(rb_cMethod, "arity", am_method_arity, 0);
95
+ }
@@ -0,0 +1,6 @@
1
+ require "mkmf"
2
+
3
+ $CFLAGS='-Werror'
4
+ extension_name = "appmap"
5
+ dir_config(extension_name)
6
+ create_makefile(File.join(extension_name, extension_name))
@@ -16,6 +16,9 @@ require 'appmap/metadata'
16
16
  require 'appmap/util'
17
17
  require 'appmap/open'
18
18
 
19
+ # load extension
20
+ require 'appmap/appmap'
21
+
19
22
  module AppMap
20
23
  class << self
21
24
  @configuration = nil
@@ -61,7 +61,7 @@ module AppMap
61
61
  location: location,
62
62
  static: static,
63
63
  labels: labels
64
- }.delete_if {|k,v| v.nil?}
64
+ }.delete_if { |k,v| v.nil? || v == [] }
65
65
  end
66
66
  end
67
67
  end
@@ -100,11 +100,16 @@ module AppMap
100
100
  static: static
101
101
  }
102
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
103
+
104
+ function_info[:location] = \
105
+ if location
106
+ location_file, lineno = location
107
+ location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
108
+ [ location_file, lineno ].join(':')
109
+ else
110
+ [ method.defined_class, static ? '.' : '#', method.name ].join
111
+ end
112
+
108
113
  function_info[:labels] = package.labels if package.labels
109
114
  object_infos << function_info
110
115
 
@@ -1,31 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppMap
4
- Package = Struct.new(:path, :package_name, :exclude, :labels) do
5
- def initialize(path, package_name, exclude, labels = nil)
6
- super
4
+ class Config
5
+ Package = Struct.new(:path, :package_name, :exclude, :labels) do
6
+ def initialize(path, package_name: nil, exclude: [], labels: [])
7
+ super path, package_name, exclude, labels
8
+ end
9
+
10
+ def to_h
11
+ {
12
+ path: path,
13
+ package_name: package_name,
14
+ exclude: exclude.blank? ? nil : exclude,
15
+ labels: labels.blank? ? nil : labels
16
+ }.compact
17
+ end
7
18
  end
8
19
 
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
20
+ Hook = Struct.new(:method_names, :package) do
16
21
  end
17
- end
18
22
 
19
- class Config
23
+ OPENSSL_PACKAGE = Package.new('openssl', package_name: 'openssl', labels: %w[security crypto])
24
+
20
25
  # Methods that should always be hooked, with their containing
21
26
  # package and labels that should be applied to them.
22
27
  HOOKED_METHODS = {
23
- 'ActiveSupport::SecurityUtils' => {
24
- secure_compare: Package.new('active_support', nil, nil, ['security'])
25
- }
26
- }
28
+ 'ActiveSupport::SecurityUtils' => Hook.new(:secure_compare, Package.new('active_support', package_name: 'active_support', labels: %w[security crypto]))
29
+ }.freeze
30
+
31
+ BUILTIN_METHODS = {
32
+ 'OpenSSL::PKey::PKey' => Hook.new(:sign, OPENSSL_PACKAGE),
33
+ 'Digest::Instance' => Hook.new(:digest, OPENSSL_PACKAGE),
34
+ 'OpenSSL::X509::Request' => Hook.new(%i[sign verify], OPENSSL_PACKAGE),
35
+ 'OpenSSL::PKCS5' => Hook.new(%i[pbkdf2_hmac_sha1 pbkdf2_hmac], OPENSSL_PACKAGE),
36
+ 'OpenSSL::Cipher' => Hook.new(%i[encrypt decrypt final], OPENSSL_PACKAGE),
37
+ 'OpenSSL::X509::Certificate' => Hook.new(:sign, OPENSSL_PACKAGE),
38
+ 'Logger' => Hook.new(:add, Package.new('logger', labels: %w[log io])),
39
+ 'Net::HTTP' => Hook.new(:request, Package.new('net/http', package_name: 'net/http', labels: %w[http io])),
40
+ 'Net::SMTP' => Hook.new(:send, Package.new('net/smtp', package_name: 'net/smtp', labels: %w[smtp email io])),
41
+ 'Net::POP3' => Hook.new(:mails, Package.new('net/pop3', package_name: 'net/pop', labels: %w[pop pop3 email io])),
42
+ 'Net::IMAP' => Hook.new(:send_command, Package.new('net/imap', package_name: 'net/imap', labels: %w[imap email io])),
43
+ 'IO' => Hook.new(%i[read write open close], Package.new('io', labels: %w[io])),
44
+ 'Marshal' => Hook.new(%i[dump load], Package.new('marshal', labels: %w[serialization marshal])),
45
+ 'Psych' => Hook.new(%i[dump dump_stream load load_stream parse parse_stream], Package.new('yaml', package_name: 'psych', labels: %w[serialization yaml])),
46
+ 'JSON::Ext::Parser' => Hook.new(:parse, Package.new('json', package_name: 'json', labels: %w[serialization json])),
47
+ 'JSON::Ext::Generator::State' => Hook.new(:generate, Package.new('json', package_name: 'json', labels: %w[serialization json]))
48
+ }.freeze
27
49
 
28
50
  attr_reader :name, :packages
51
+
29
52
  def initialize(name, packages = [])
30
53
  @name = name
31
54
  @packages = packages
@@ -41,7 +64,7 @@ module AppMap
41
64
  # Loads configuration from a Hash.
42
65
  def load(config_data)
43
66
  packages = (config_data['packages'] || []).map do |package|
44
- Package.new(package['path'], nil, package['exclude'] || [])
67
+ Package.new(package['path'], exclude: package['exclude'] || [])
45
68
  end
46
69
  Config.new config_data['name'], packages
47
70
  end
@@ -55,9 +78,9 @@ module AppMap
55
78
  end
56
79
 
57
80
  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
81
+ defined_class, _, method_name = ::AppMap::Hook.qualify_method_name(method)
82
+ package = find_package(defined_class, method_name)
83
+ return package if package
61
84
 
62
85
  location = method.source_location
63
86
  location_file, = location
@@ -75,15 +98,18 @@ module AppMap
75
98
  end
76
99
 
77
100
  def always_hook?(defined_class, method_name)
78
- !!find_hooked_method(defined_class, method_name)
101
+ !!find_package(defined_class, method_name)
79
102
  end
80
103
 
81
- def find_hooked_method(defined_class, method_name)
82
- find_hooked_class(defined_class)[method_name]
104
+ def find_package(defined_class, method_name)
105
+ hook = find_hook(defined_class)
106
+ return nil unless hook
107
+
108
+ Array(hook.method_names).include?(method_name) ? hook.package : nil
83
109
  end
84
110
 
85
- def find_hooked_class(defined_class)
86
- HOOKED_METHODS[defined_class] || {}
111
+ def find_hook(defined_class)
112
+ HOOKED_METHODS[defined_class] || BUILTIN_METHODS[defined_class]
87
113
  end
88
114
  end
89
115
  end
@@ -4,6 +4,8 @@ require 'appmap/util'
4
4
 
5
5
  module AppMap
6
6
  module Cucumber
7
+ APPMAP_OUTPUT_DIR = 'tmp/appmap/cucumber'
8
+
7
9
  ScenarioAttributes = Struct.new(:name, :feature, :feature_group)
8
10
 
9
11
  ProviderStruct = Struct.new(:scenario) do
@@ -38,18 +40,27 @@ module AppMap
38
40
  end
39
41
 
40
42
  class << self
43
+ def init
44
+ warn 'Configuring AppMap recorder for Cucumber'
45
+
46
+ FileUtils.mkdir_p APPMAP_OUTPUT_DIR
47
+ end
48
+
41
49
  def write_scenario(scenario, appmap)
42
50
  appmap['metadata'] = update_metadata(scenario, appmap['metadata'])
43
51
  scenario_filename = AppMap::Util.scenario_filename(appmap['metadata']['name'])
44
52
 
45
- FileUtils.mkdir_p 'tmp/appmap/cucumber'
46
- File.write(File.join('tmp/appmap/cucumber', scenario_filename), JSON.generate(appmap))
53
+ File.write(File.join(APPMAP_OUTPUT_DIR, scenario_filename), JSON.generate(appmap))
47
54
  end
48
55
 
49
56
  def enabled?
50
57
  ENV['APPMAP'] == 'true'
51
58
  end
52
59
 
60
+ def run
61
+ init
62
+ end
63
+
53
64
  protected
54
65
 
55
66
  def cucumber_version
@@ -87,3 +98,9 @@ module AppMap
87
98
  end
88
99
  end
89
100
  end
101
+
102
+ if AppMap::Cucumber.enabled?
103
+ require 'appmap'
104
+
105
+ AppMap::Cucumber.run
106
+ end