appmap 0.33.0 → 0.34.5

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.
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