appmap 0.32.0 → 0.34.4

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