appmap 0.32.0 → 0.34.4

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: f6becd9530caf01e14b8a7db69e74b40297d41e4724f25315cd7b44ea6ad3e02
4
- data.tar.gz: 2863e714f0651d73fc9f2f87d0ea2c5f9fb7f9f553d1f109d9d8f10fff35f5be
3
+ metadata.gz: a3a509a94faa2a85ff11702ae55ab3df975f04416d93d169b0a24989fd520b9d
4
+ data.tar.gz: 3895b9c788413f85738ba487e717bf48e4dfc2e8c0792f30c6f9f6ba7dc5e9c8
5
5
  SHA512:
6
- metadata.gz: a5574478fee68b2531449df2b9ec135a60110950cdcd518cf5b85d10e6d0d756b3e22740a4ce0905d7fccb2e6e7d684ee38ac61fd11ad6fdfcff4355a7ae7b36
7
- data.tar.gz: d15827a809ffde6b6ff98bfb62057f759cdc03e7e24452c1a8273801f94e37d61bab265ddb863ce8f60d83c639875fcf2d247f27abe9b27164817f0739b7c699
6
+ metadata.gz: 07fe201395cad27e731402b5b2ff0efd653056dad16b9301657034df0b6e7fd3a205768ef3656874f55109edfc4d788d752f8bc73f4ff6874c815104547ce281
7
+ data.tar.gz: cbbd1551c352519954e9ddc9dd27279d6d232663fb67fd5ed4369a603a382f140593aa195aa08f35a57b0a14d38bef07bfef0aa7bd942d660a865fb3682d500d
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,31 @@
1
+ # v0.34.4
2
+ * Make sure `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.server_version` only calls
3
+ `ActiveRecord::Base.connection.database_version` if it's available.
4
+ * Fix `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.database_type` returns `:postgres`
5
+ in all supported versions of Rails.
6
+
7
+ # v0.34.3
8
+ * Fix a crash in `singleton_method_owner_name` that occurred if `__attached__.class` returned
9
+ something other than a `Module` or a `Class`.
10
+
11
+ # v0.34.2
12
+ * Add an extension that gets the name of the owner of a singleton method without calling
13
+ any methods that may have been redefined (e.g. `#to_s` or `.name`).
14
+
15
+ # v0.34.1
16
+ * Ensure that capturing events doesn't change the behavior of a hooked method that uses
17
+ `Time.now`. For example, if a test expects that `Time.now` will be called a certain
18
+ number of times by a hooked method, that expectation will now be met.
19
+ * Make sure `appmap/cucumber` requires `appmap`.
20
+
21
+ # v0.34.0
22
+
23
+ * Records builtin security and I/O methods from `OpenSSL`, `Net`, and `IO`.
24
+
25
+ # v0.33.0
26
+
27
+ * Added command `AppMap.open` to open an AppMap in the browser.
28
+
1
29
  # v0.32.0
2
30
 
3
31
  * Removes un-necessary fields from `return` events.
data/README.md CHANGED
@@ -267,7 +267,13 @@ $ bundle config local.appmap $(pwd)
267
267
  Run the tests via `rake`:
268
268
  ```
269
269
  $ bundle exec rake test
270
- ```
270
+ ```
271
+
272
+ The `test` target will build the native extension first, then run the tests. If you need
273
+ to build the extension separately, run
274
+ ```
275
+ $ bundle exec rake compile
276
+ ```
271
277
 
272
278
  ## Using fixture apps
273
279
 
@@ -286,7 +292,7 @@ resources such as a PostgreSQL database.
286
292
  To build the fixture container images, first run:
287
293
 
288
294
  ```sh-session
289
- $ bundle exec rake fixtures:all
295
+ $ bundle exec rake build:fixtures:all
290
296
  ```
291
297
 
292
298
  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']
@@ -26,6 +28,7 @@ Gem::Specification.new do |spec|
26
28
  spec.add_dependency 'faraday'
27
29
  spec.add_dependency 'gli'
28
30
  spec.add_dependency 'parser'
31
+ spec.add_dependency 'rack'
29
32
 
30
33
  spec.add_development_dependency 'bundler', '~> 1.16'
31
34
  spec.add_development_dependency 'minitest', '~> 5.0'
@@ -33,6 +36,7 @@ Gem::Specification.new do |spec|
33
36
  spec.add_development_dependency 'rake', '>= 12.3.3'
34
37
  spec.add_development_dependency 'rdoc'
35
38
  spec.add_development_dependency 'rubocop'
39
+ spec.add_development_dependency "rake-compiler"
36
40
 
37
41
  # Testing
38
42
  spec.add_development_dependency 'climate_control'
@@ -41,4 +45,6 @@ Gem::Specification.new do |spec|
41
45
  spec.add_development_dependency 'rspec'
42
46
  spec.add_development_dependency 'selenium-webdriver'
43
47
  spec.add_development_dependency 'webdrivers', '~> 4.0'
48
+ spec.add_development_dependency 'timecop'
49
+ spec.add_development_dependency 'hashie'
44
50
  end
@@ -0,0 +1,36 @@
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
+ static VALUE singleton_method_owner_name(VALUE klass, VALUE method)
12
+ {
13
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
14
+ VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
15
+ if (!CLASS_OR_MODULE_P(attached)) {
16
+ attached = rb_funcall(attached, rb_intern("class"), 0);
17
+ }
18
+
19
+ // Did __attached__.class return an object that's a Module or a
20
+ // Class?
21
+ if (CLASS_OR_MODULE_P(attached)) {
22
+ // Yup, get it's name
23
+ return rb_mod_name(attached);
24
+ }
25
+
26
+ // Nope (which seems weird, but whatever). Fall back to calling
27
+ // #to_s on the method's owner and hope for the best.
28
+ return rb_funcall(owner, rb_intern("to_s"), 0);
29
+ }
30
+
31
+ void Init_appmap() {
32
+ VALUE appmap = rb_define_module("AppMap");
33
+ VALUE hook = rb_define_class_under(appmap, "Hook", rb_cObject);
34
+
35
+ rb_define_singleton_method(hook, "singleton_method_owner_name", singleton_method_owner_name, 1);
36
+ }
@@ -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))
@@ -14,19 +14,23 @@ require 'appmap/trace'
14
14
  require 'appmap/class_map'
15
15
  require 'appmap/metadata'
16
16
  require 'appmap/util'
17
+ require 'appmap/open'
18
+
19
+ # load extension
20
+ require 'appmap/appmap'
17
21
 
18
22
  module AppMap
19
23
  class << self
20
24
  @configuration = nil
21
25
  @configuration_file_path = nil
22
26
 
23
- # configuration gets the configuration. If there is no configuration, the default
27
+ # Gets the configuration. If there is no configuration, the default
24
28
  # configuration is initialized.
25
29
  def configuration
26
- @configuration ||= configure
30
+ @configuration ||= initialize
27
31
  end
28
32
 
29
- # configuration= sets the configuration. This is only expected to happen once per
33
+ # Sets the configuration. This is only expected to happen once per
30
34
  # Ruby process.
31
35
  def configuration=(config)
32
36
  warn 'AppMap is already configured' if @configuration && config
@@ -34,22 +38,24 @@ module AppMap
34
38
  @configuration = config
35
39
  end
36
40
 
37
- # initialize configures AppMap for recording. Default behavior is to configure from "appmap.yml".
41
+ # Configures AppMap for recording. Default behavior is to configure from "appmap.yml".
38
42
  # This method also activates the code hooks which record function calls as trace events.
39
43
  # Call this function before the program code is loaded by the Ruby VM, otherwise
40
44
  # the load events won't be seen and the hooks won't activate.
41
45
  def initialize(config_file_path = 'appmap.yml')
42
46
  warn "Configuring AppMap from path #{config_file_path}"
43
- self.configuration = Config.load_from_file(config_file_path)
44
- Hook.new(configuration).enable
47
+ Config.load_from_file(config_file_path).tap do |configuration|
48
+ self.configuration = configuration
49
+ Hook.new(configuration).enable
50
+ end
45
51
  end
46
52
 
47
- # tracing can be used to start tracing, stop tracing, and record events.
53
+ # Used to start tracing, stop tracing, and record events.
48
54
  def tracing
49
55
  @tracing ||= Trace::Tracing.new
50
56
  end
51
57
 
52
- # record records the events which occur while processing a block,
58
+ # Records the events which occur while processing a block,
53
59
  # and returns an AppMap as a Hash.
54
60
  def record
55
61
  tracer = tracing.trace
@@ -70,12 +76,18 @@ module AppMap
70
76
  }
71
77
  end
72
78
 
73
- # class_map builds a class map from a config and a list of Ruby methods.
79
+ # Uploads an AppMap to the AppLand website and displays it.
80
+ def open(appmap = nil, &block)
81
+ appmap ||= AppMap.record(&block)
82
+ AppMap::Open.new(appmap).perform
83
+ end
84
+
85
+ # Builds a class map from a config and a list of Ruby methods.
74
86
  def class_map(methods)
75
87
  ClassMap.build_from_methods(configuration, methods)
76
88
  end
77
89
 
78
- # detect_metadata returns default metadata detected from the Ruby system and from the
90
+ # Returns default metadata detected from the Ruby system and from the
79
91
  # filesystem.
80
92
  def detect_metadata
81
93
  @metadata ||= Metadata.detect.freeze
@@ -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