appmap 0.31.0 → 0.34.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -1
  3. data/.rbenv-gemsets +1 -0
  4. data/CHANGELOG.md +22 -0
  5. data/README.md +38 -4
  6. data/Rakefile +10 -3
  7. data/appmap.gemspec +5 -0
  8. data/ext/appmap/appmap.c +26 -0
  9. data/ext/appmap/extconf.rb +6 -0
  10. data/lib/appmap.rb +23 -10
  11. data/lib/appmap/class_map.rb +13 -7
  12. data/lib/appmap/config.rb +54 -30
  13. data/lib/appmap/cucumber.rb +19 -2
  14. data/lib/appmap/event.rb +25 -16
  15. data/lib/appmap/hook.rb +52 -77
  16. data/lib/appmap/hook/method.rb +103 -0
  17. data/lib/appmap/open.rb +57 -0
  18. data/lib/appmap/rails/action_handler.rb +7 -7
  19. data/lib/appmap/rails/sql_handler.rb +10 -8
  20. data/lib/appmap/rspec.rb +1 -1
  21. data/lib/appmap/trace.rb +7 -7
  22. data/lib/appmap/util.rb +19 -0
  23. data/lib/appmap/version.rb +1 -1
  24. data/spec/abstract_controller4_base_spec.rb +1 -1
  25. data/spec/abstract_controller_base_spec.rb +9 -2
  26. data/spec/fixtures/hook/instance_method.rb +4 -0
  27. data/spec/fixtures/hook/singleton_method.rb +21 -12
  28. data/spec/hook_spec.rb +140 -44
  29. data/spec/open_spec.rb +19 -0
  30. data/spec/record_sql_rails_pg_spec.rb +56 -33
  31. data/test/cli_test.rb +12 -2
  32. data/test/fixtures/openssl_recorder/Gemfile +3 -0
  33. data/test/fixtures/openssl_recorder/appmap.yml +3 -0
  34. data/{spec/fixtures/hook/openssl_sign.rb → test/fixtures/openssl_recorder/lib/openssl_cert_sign.rb} +11 -4
  35. data/test/fixtures/openssl_recorder/lib/openssl_encrypt.rb +34 -0
  36. data/test/fixtures/openssl_recorder/lib/openssl_key_sign.rb +28 -0
  37. data/test/openssl_test.rb +203 -0
  38. data/test/test_helper.rb +1 -0
  39. metadata +58 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54b9a05aeb3eea84572b115a6de1106e6743b74046048e3c168f99a75aa9e669
4
- data.tar.gz: eb8a690376d833c4bec9f0789c5499f413f7dcb3b1ef111257785bd344861f64
3
+ metadata.gz: 81605d2c95140e963991e04af89235ee903cce0e3a86b2477f0f80961704e2d6
4
+ data.tar.gz: 7e526d9ffbc559bb3968b8a973dcd757147676d17a5028f737192fd8956d1b43
5
5
  SHA512:
6
- metadata.gz: 453f06041220ecd0a4fc563ae007e7f8247d3dfcccf49657cbf47ddc53d0175e6834d1dbd89a2338a032d6cfae866929216b58a11bb6f0fe3fd2ea69508d7b2a
7
- data.tar.gz: 77f372ad255c3c664c690affa2962958cebd1e8f34579809e4d975fdf57404889d8506f9153d44a95afda297ffe7b1654529252a5293487477e803bdf0cc9608
6
+ metadata.gz: 6a34ba2662d48a7e0083e16d6abe4693b57711f9ac0020da6631ca6c40172aeb8f3ef734edd504cec00f2f437742e45fc0cf631b425cb960c9d51239556bc461
7
+ data.tar.gz: 156f126aac6f63e819c175d6b4d481679cbced9184007871c12fb174f187198afd8e7788befc4ab0ff35b10293e1e163eee25a985b35bd93853c03aa8268d192
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,25 @@
1
+ # v0.34.2
2
+ * Add an extension that gets the name of the owner of a singleton method without calling
3
+ any methods that may have been redefined (e.g. `#to_s` or `.name`).
4
+
5
+ # v0.34.1
6
+ * Ensure that capturing events doesn't change the behavior of a hooked method that uses
7
+ `Time.now`. For example, if a test expects that `Time.now` will be called a certain
8
+ number of times by a hooked method, that expectation will now be met.
9
+ * Make sure `appmap/cucumber` requires `appmap`.
10
+
11
+ # v0.34.0
12
+
13
+ * Records builtin security and I/O methods from `OpenSSL`, `Net`, and `IO`.
14
+
15
+ # v0.33.0
16
+
17
+ * Added command `AppMap.open` to open an AppMap in the browser.
18
+
19
+ # v0.32.0
20
+
21
+ * Removes un-necessary fields from `return` events.
22
+
1
23
  # v0.31.0
2
24
 
3
25
  * Add the ability to hook methods by default, and optionally add labels to them in the
data/README.md CHANGED
@@ -33,7 +33,7 @@ There are several ways to record AppMaps of your Ruby program using the `appmap`
33
33
 
34
34
  Once you have recorded some AppMaps (for example, by running RSpec tests), you use the `appland upload` command
35
35
  to upload them to the AppLand server. This command, and some others, is provided
36
- by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases), to
36
+ by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases).
37
37
  Then, on the [AppLand website](https://app.land), you can
38
38
  visualize the design of your code and share links with collaborators.
39
39
 
@@ -87,12 +87,25 @@ Each entry in the `packages` list is a YAML object which has the following keys:
87
87
 
88
88
  To record RSpec tests, follow these additional steps:
89
89
 
90
- 1) Require `appmap/rspec` in your `spec_helper.rb`.
90
+ 1) Require `appmap/rspec` in your `spec_helper.rb` before any other classes are loaded.
91
91
 
92
92
  ```ruby
93
93
  require 'appmap/rspec'
94
94
  ```
95
95
 
96
+ Note that `spec_helper.rb` in a Rails project typically loads the application's classes this way:
97
+
98
+ ```ruby
99
+ require File.expand_path("../../config/environment", __FILE__)
100
+ ```
101
+
102
+ and `appmap/rspec` must be required before this:
103
+
104
+ ```ruby
105
+ require 'appmap/rspec'
106
+ require File.expand_path("../../config/environment", __FILE__)
107
+ ```
108
+
96
109
  2) *Optional* Add `feature: '<feature name>'` and `feature_group: '<feature group name>'` annotations to your
97
110
  examples.
98
111
 
@@ -136,6 +149,19 @@ To record Minitest tests, follow these additional steps:
136
149
  require 'appmap/minitest'
137
150
  ```
138
151
 
152
+ Note that `test_helper.rb` in a Rails project typically loads the application's classes this way:
153
+
154
+ ```ruby
155
+ require_relative '../config/environment'
156
+ ```
157
+
158
+ and `appmap/rspec` must be required before this:
159
+
160
+ ```ruby
161
+ require 'appmap/rspec'
162
+ require_relative '../config/environment'
163
+ ```
164
+
139
165
  2) Run the tests with the environment variable `APPMAP=true`:
140
166
 
141
167
  ```sh-session
@@ -159,6 +185,8 @@ To record Cucumber tests, follow these additional steps:
159
185
  require 'appmap/cucumber'
160
186
  ```
161
187
 
188
+ Be sure to require it before `config/environment` is required.
189
+
162
190
  2) Create an `Around` hook in `support/hooks.rb` to record the scenario:
163
191
 
164
192
 
@@ -239,7 +267,13 @@ $ bundle config local.appmap $(pwd)
239
267
  Run the tests via `rake`:
240
268
  ```
241
269
  $ bundle exec rake test
242
- ```
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
+ ```
243
277
 
244
278
  ## Using fixture apps
245
279
 
@@ -258,7 +292,7 @@ resources such as a PostgreSQL database.
258
292
  To build the fixture container images, first run:
259
293
 
260
294
  ```sh-session
261
- $ bundle exec rake fixtures:all
295
+ $ bundle exec rake build:fixtures:all
262
296
  ```
263
297
 
264
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,5 @@ 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'
44
49
  end
@@ -0,0 +1,26 @@
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
+ return rb_mod_name(attached);
19
+ }
20
+
21
+ void Init_appmap() {
22
+ VALUE appmap = rb_define_module("AppMap");
23
+ VALUE hook = rb_define_class_under(appmap, "Hook", rb_cObject);
24
+
25
+ rb_define_singleton_method(hook, "singleton_method_owner_name", singleton_method_owner_name, 1);
26
+ }
@@ -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))
@@ -13,19 +13,24 @@ require 'appmap/config'
13
13
  require 'appmap/trace'
14
14
  require 'appmap/class_map'
15
15
  require 'appmap/metadata'
16
+ require 'appmap/util'
17
+ require 'appmap/open'
18
+
19
+ # load extension
20
+ require 'appmap/appmap'
16
21
 
17
22
  module AppMap
18
23
  class << self
19
24
  @configuration = nil
20
25
  @configuration_file_path = nil
21
26
 
22
- # configuration gets the configuration. If there is no configuration, the default
27
+ # Gets the configuration. If there is no configuration, the default
23
28
  # configuration is initialized.
24
29
  def configuration
25
- @configuration ||= configure
30
+ @configuration ||= initialize
26
31
  end
27
32
 
28
- # configuration= sets the configuration. This is only expected to happen once per
33
+ # Sets the configuration. This is only expected to happen once per
29
34
  # Ruby process.
30
35
  def configuration=(config)
31
36
  warn 'AppMap is already configured' if @configuration && config
@@ -33,22 +38,24 @@ module AppMap
33
38
  @configuration = config
34
39
  end
35
40
 
36
- # 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".
37
42
  # This method also activates the code hooks which record function calls as trace events.
38
43
  # Call this function before the program code is loaded by the Ruby VM, otherwise
39
44
  # the load events won't be seen and the hooks won't activate.
40
45
  def initialize(config_file_path = 'appmap.yml')
41
46
  warn "Configuring AppMap from path #{config_file_path}"
42
- self.configuration = Config.load_from_file(config_file_path)
43
- 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
44
51
  end
45
52
 
46
- # tracing can be used to start tracing, stop tracing, and record events.
53
+ # Used to start tracing, stop tracing, and record events.
47
54
  def tracing
48
55
  @tracing ||= Trace::Tracing.new
49
56
  end
50
57
 
51
- # record records the events which occur while processing a block,
58
+ # Records the events which occur while processing a block,
52
59
  # and returns an AppMap as a Hash.
53
60
  def record
54
61
  tracer = tracing.trace
@@ -69,12 +76,18 @@ module AppMap
69
76
  }
70
77
  end
71
78
 
72
- # 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.
73
86
  def class_map(methods)
74
87
  ClassMap.build_from_methods(configuration, methods)
75
88
  end
76
89
 
77
- # 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
78
91
  # filesystem.
79
92
  def detect_metadata
80
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
@@ -80,10 +80,6 @@ module AppMap
80
80
  protected
81
81
 
82
82
  def add_function(root, package, method)
83
- location = method.source_location
84
- location_file, lineno = location
85
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
86
-
87
83
  static = method.static
88
84
 
89
85
  object_infos = [
@@ -101,12 +97,22 @@ module AppMap
101
97
  function_info = {
102
98
  name: method.name,
103
99
  type: 'function',
104
- location: [ location_file, lineno ].join(':'),
105
100
  static: static
106
101
  }
102
+ location = method.source_location
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
+
107
113
  function_info[:labels] = package.labels if package.labels
108
114
  object_infos << function_info
109
-
115
+
110
116
  parent = root
111
117
  object_infos.each do |info|
112
118
  parent = find_or_create parent.children, info do
@@ -1,33 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppMap
4
- Package = Struct.new(:path, :exclude, :labels) do
5
- def initialize(path, 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
-
9
- def to_h
10
- {
11
- path: path,
12
- exclude: exclude.blank? ? nil : exclude,
13
- labels: labels.blank? ? nil : labels
14
- }.compact
19
+
20
+ Hook = Struct.new(:method_names, :package) do
15
21
  end
16
- end
17
22
 
18
- class Config
23
+ OPENSSL_PACKAGE = Package.new('openssl', package_name: 'openssl', labels: %w[security crypto])
24
+
19
25
  # Methods that should always be hooked, with their containing
20
26
  # package and labels that should be applied to them.
21
27
  HOOKED_METHODS = {
22
- 'ActiveSupport::SecurityUtils' => {
23
- secure_compare: Package.new('active_support', nil, ['security'])
24
- },
25
- 'OpenSSL::X509::Certificate' => {
26
- sign: Package.new('openssl', nil, ['security'])
27
- }
28
- }
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
29
49
 
30
50
  attr_reader :name, :packages
51
+
31
52
  def initialize(name, packages = [])
32
53
  @name = name
33
54
  @packages = packages
@@ -43,7 +64,7 @@ module AppMap
43
64
  # Loads configuration from a Hash.
44
65
  def load(config_data)
45
66
  packages = (config_data['packages'] || []).map do |package|
46
- Package.new(package['path'], package['exclude'] || [])
67
+ Package.new(package['path'], exclude: package['exclude'] || [])
47
68
  end
48
69
  Config.new config_data['name'], packages
49
70
  end
@@ -57,14 +78,14 @@ module AppMap
57
78
  end
58
79
 
59
80
  def package_for_method(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
84
+
60
85
  location = method.source_location
61
86
  location_file, = location
62
87
  return unless location_file
63
88
 
64
- defined_class,_,method_name = Hook.qualify_method_name(method)
65
- hooked_method = find_hooked_method(defined_class, method_name)
66
- return hooked_method if hooked_method
67
-
68
89
  location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
69
90
  packages.find do |pkg|
70
91
  (location_file.index(pkg.path) == 0) &&
@@ -77,15 +98,18 @@ module AppMap
77
98
  end
78
99
 
79
100
  def always_hook?(defined_class, method_name)
80
- !!find_hooked_method(defined_class, method_name)
101
+ !!find_package(defined_class, method_name)
81
102
  end
82
103
 
83
- def find_hooked_method(defined_class, method_name)
84
- 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
85
109
  end
86
-
87
- def find_hooked_class(defined_class)
88
- HOOKED_METHODS[defined_class] || {}
110
+
111
+ def find_hook(defined_class)
112
+ HOOKED_METHODS[defined_class] || BUILTIN_METHODS[defined_class]
89
113
  end
90
114
  end
91
115
  end