appmap 0.28.1 → 0.34.1

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -1
  3. data/README.md +54 -2
  4. data/Rakefile +1 -1
  5. data/appmap.gemspec +2 -0
  6. data/lib/appmap.rb +25 -14
  7. data/lib/appmap/class_map.rb +25 -27
  8. data/lib/appmap/config.rb +115 -0
  9. data/lib/appmap/cucumber.rb +19 -2
  10. data/lib/appmap/event.rb +25 -16
  11. data/lib/appmap/hook.rb +89 -139
  12. data/lib/appmap/hook/method.rb +83 -0
  13. data/lib/appmap/metadata.rb +1 -1
  14. data/lib/appmap/minitest.rb +141 -0
  15. data/lib/appmap/open.rb +57 -0
  16. data/lib/appmap/rails/action_handler.rb +7 -7
  17. data/lib/appmap/rails/sql_handler.rb +10 -8
  18. data/lib/appmap/record.rb +27 -0
  19. data/lib/appmap/rspec.rb +2 -2
  20. data/lib/appmap/trace.rb +16 -8
  21. data/lib/appmap/util.rb +19 -0
  22. data/lib/appmap/version.rb +1 -1
  23. data/spec/abstract_controller4_base_spec.rb +1 -1
  24. data/spec/abstract_controller_base_spec.rb +9 -2
  25. data/spec/config_spec.rb +3 -3
  26. data/spec/fixtures/hook/compare.rb +7 -0
  27. data/spec/fixtures/hook/instance_method.rb +4 -0
  28. data/spec/hook_spec.rb +222 -37
  29. data/spec/open_spec.rb +19 -0
  30. data/spec/record_sql_rails_pg_spec.rb +56 -33
  31. data/spec/util_spec.rb +1 -1
  32. data/test/cli_test.rb +12 -2
  33. data/test/fixtures/minitest_recorder/Gemfile +5 -0
  34. data/test/fixtures/minitest_recorder/appmap.yml +3 -0
  35. data/test/fixtures/minitest_recorder/lib/hello.rb +5 -0
  36. data/test/fixtures/minitest_recorder/test/hello_test.rb +12 -0
  37. data/test/fixtures/openssl_recorder/Gemfile +3 -0
  38. data/test/fixtures/openssl_recorder/appmap.yml +3 -0
  39. data/test/fixtures/openssl_recorder/lib/openssl_cert_sign.rb +94 -0
  40. data/test/fixtures/openssl_recorder/lib/openssl_encrypt.rb +34 -0
  41. data/test/fixtures/openssl_recorder/lib/openssl_key_sign.rb +28 -0
  42. data/test/fixtures/process_recorder/appmap.yml +3 -0
  43. data/test/fixtures/process_recorder/hello.rb +9 -0
  44. data/test/minitest_test.rb +38 -0
  45. data/test/openssl_test.rb +203 -0
  46. data/test/record_process_test.rb +35 -0
  47. data/test/test_helper.rb +1 -0
  48. metadata +51 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8619a5924ba51ee48d76ebfe5c72030629c5338217b4fe659cd0a580eaa6ddb9
4
- data.tar.gz: 0d8b1e8fcd41e0785b362350a53c95c5bc0ba5c628371b7b6216315b7ddcd85e
3
+ metadata.gz: 39576d59d2648a009f888fa9c5720d8fdb91b04fcb4d08949a05a0a5c453d0c8
4
+ data.tar.gz: 8b1d5442421d9dd37c3db4fa0329321e6b6e6ebc87e94bb67be3da9b3f1467b0
5
5
  SHA512:
6
- metadata.gz: c4da2cadb6a852b1213d1938b035ca147a939e7dc1378ef435d1e8b4d42d6f5b98e19191ecd2e1d1e48493817117123e0b9167080308dcc9ea5527bb676f436b
7
- data.tar.gz: 04c2a36c913a3eec42dcd8aa61e789b9b91fed1e43b4bfe84729c6a7b4451fb1064c6efdba39e3818674df8f9560caa486e1d4c8170f04682938775fbb2c4d7e
6
+ metadata.gz: 40bc1a630b65a83f7516085ec4cd25a72f83e574a9c2f150f8e302c9756c376a1fc820d490393d9625aeb3f5b781ca44d34431efe2722a7a0031b49334ccbca7
7
+ data.tar.gz: 9d32eb02deebdfb16e99ee0f157f5c9de6cb77cafa46233867e0173577609d4390b98a675f67c11868d57370066666bf9f9da39b2ee2ab25ba16cbaf4ebcd8ca
@@ -1,7 +1,38 @@
1
+ # v0.34.1
2
+ * Ensure that capturing events doesn't change the behavior of a hooked method that uses
3
+ `Time.now`. For example, if a test expects that `Time.now` will be called a certain
4
+ number of times by a hooked method, that expectation will now be met.
5
+ * Make sure `appmap/cucumber` requires `appmap`.
6
+
7
+ # v0.34.0
8
+
9
+ * Records builtin security and I/O methods from `OpenSSL`, `Net`, and `IO`.
10
+
11
+ # v0.33.0
12
+
13
+ * Added command `AppMap.open` to open an AppMap in the browser.
14
+
15
+ # v0.32.0
16
+
17
+ * Removes un-necessary fields from `return` events.
18
+
19
+ # v0.31.0
20
+
21
+ * Add the ability to hook methods by default, and optionally add labels to them in the
22
+ classmap. Use it to hook `ActiveSupport::SecurityUtils.secure_compare`.
23
+
24
+ # v0.30.0
25
+
26
+ * Add support for Minitest.
27
+
28
+ # v0.29.0
29
+
30
+ * Add `lib/appmap/record.rb`, which can be `require`d to record the rest of the process.
31
+
1
32
  # v0.28.1
33
+
2
34
  * Fix the `defined_class` recorded in an appmap for an instance method included in a class
3
35
  at runtime.
4
-
5
36
  * Only include the `static` attribute on `call` events in an appmap. Determine its value
6
37
  based on the receiver of the method call.
7
38
 
data/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  - [Configuration](#configuration)
4
4
  - [Running](#running)
5
5
  - [RSpec](#rspec)
6
+ - [Minitest](#minitest)
6
7
  - [Cucumber](#cucumber)
7
8
  - [Remote recording](#remote-recording)
8
9
  - [Ruby on Rails](#ruby-on-rails)
@@ -32,7 +33,7 @@ There are several ways to record AppMaps of your Ruby program using the `appmap`
32
33
 
33
34
  Once you have recorded some AppMaps (for example, by running RSpec tests), you use the `appland upload` command
34
35
  to upload them to the AppLand server. This command, and some others, is provided
35
- by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases), to
36
+ by the [AppLand CLI](https://github.com/applandinc/appland-cli/releases).
36
37
  Then, on the [AppLand website](https://app.land), you can
37
38
  visualize the design of your code and share links with collaborators.
38
39
 
@@ -86,12 +87,25 @@ Each entry in the `packages` list is a YAML object which has the following keys:
86
87
 
87
88
  To record RSpec tests, follow these additional steps:
88
89
 
89
- 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.
90
91
 
91
92
  ```ruby
92
93
  require 'appmap/rspec'
93
94
  ```
94
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
+
95
109
  2) *Optional* Add `feature: '<feature name>'` and `feature_group: '<feature group name>'` annotations to your
96
110
  examples.
97
111
 
@@ -125,6 +139,42 @@ If you include the `feature` and `feature_group` metadata, these attributes will
125
139
 
126
140
  If you don't explicitly declare `feature` and `feature_group`, then they will be inferred from the spec name and example descriptions.
127
141
 
142
+ ## Minitest
143
+
144
+ To record Minitest tests, follow these additional steps:
145
+
146
+ 1) Require `appmap/minitest` in `test_helper.rb`
147
+
148
+ ```ruby
149
+ require 'appmap/minitest'
150
+ ```
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
+
165
+ 2) Run the tests with the environment variable `APPMAP=true`:
166
+
167
+ ```sh-session
168
+ $ APPMAP=true bundle exec -Ilib -Itest test/*
169
+ ```
170
+
171
+ Each Minitest test will output an AppMap file into the directory `tmp/appmap/minitest`. For example:
172
+
173
+ ```
174
+ $ find tmp/appmap/minitest
175
+ Hello_says_hello_when_prompted.appmap.json
176
+ ```
177
+
128
178
  ## Cucumber
129
179
 
130
180
  To record Cucumber tests, follow these additional steps:
@@ -135,6 +185,8 @@ To record Cucumber tests, follow these additional steps:
135
185
  require 'appmap/cucumber'
136
186
  ```
137
187
 
188
+ Be sure to require it before `config/environment` is required.
189
+
138
190
  2) Create an `Around` hook in `support/hooks.rb` to record the scenario:
139
191
 
140
192
 
data/Rakefile CHANGED
@@ -122,7 +122,7 @@ end
122
122
  Rake::TestTask.new(:minitest) do |t|
123
123
  t.libs << 'test'
124
124
  t.libs << 'lib'
125
- t.test_files = FileList['test/**/*_test.rb']
125
+ t.test_files = FileList['test/*_test.rb']
126
126
  end
127
127
 
128
128
  task spec: "spec:all"
@@ -26,6 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency 'faraday'
27
27
  spec.add_dependency 'gli'
28
28
  spec.add_dependency 'parser'
29
+ spec.add_dependency 'rack'
29
30
 
30
31
  spec.add_development_dependency 'bundler', '~> 1.16'
31
32
  spec.add_development_dependency 'minitest', '~> 5.0'
@@ -41,4 +42,5 @@ Gem::Specification.new do |spec|
41
42
  spec.add_development_dependency 'rspec'
42
43
  spec.add_development_dependency 'selenium-webdriver'
43
44
  spec.add_development_dependency 'webdrivers', '~> 4.0'
45
+ spec.add_development_dependency 'timecop'
44
46
  end
@@ -8,19 +8,26 @@ rescue NameError
8
8
  end
9
9
 
10
10
  require 'appmap/version'
11
+ require 'appmap/hook'
12
+ require 'appmap/config'
13
+ require 'appmap/trace'
14
+ require 'appmap/class_map'
15
+ require 'appmap/metadata'
16
+ require 'appmap/util'
17
+ require 'appmap/open'
11
18
 
12
19
  module AppMap
13
20
  class << self
14
21
  @configuration = nil
15
22
  @configuration_file_path = nil
16
23
 
17
- # configuration gets the configuration. If there is no configuration, the default
24
+ # Gets the configuration. If there is no configuration, the default
18
25
  # configuration is initialized.
19
26
  def configuration
20
- @configuration ||= configure
27
+ @configuration ||= initialize
21
28
  end
22
29
 
23
- # configuration= sets the configuration. This is only expected to happen once per
30
+ # Sets the configuration. This is only expected to happen once per
24
31
  # Ruby process.
25
32
  def configuration=(config)
26
33
  warn 'AppMap is already configured' if @configuration && config
@@ -28,24 +35,24 @@ module AppMap
28
35
  @configuration = config
29
36
  end
30
37
 
31
- # initialize configures AppMap for recording. Default behavior is to configure from "appmap.yml".
38
+ # Configures AppMap for recording. Default behavior is to configure from "appmap.yml".
32
39
  # This method also activates the code hooks which record function calls as trace events.
33
40
  # Call this function before the program code is loaded by the Ruby VM, otherwise
34
41
  # the load events won't be seen and the hooks won't activate.
35
42
  def initialize(config_file_path = 'appmap.yml')
36
43
  warn "Configuring AppMap from path #{config_file_path}"
37
- require 'appmap/hook'
38
- self.configuration = Hook::Config.load_from_file(config_file_path)
39
- Hook.hook(configuration)
44
+ Config.load_from_file(config_file_path).tap do |configuration|
45
+ self.configuration = configuration
46
+ Hook.new(configuration).enable
47
+ end
40
48
  end
41
49
 
42
- # tracing can be used to start tracing, stop tracing, and record events.
50
+ # Used to start tracing, stop tracing, and record events.
43
51
  def tracing
44
- require 'appmap/trace'
45
52
  @tracing ||= Trace::Tracing.new
46
53
  end
47
54
 
48
- # record records the events which occur while processing a block,
55
+ # Records the events which occur while processing a block,
49
56
  # and returns an AppMap as a Hash.
50
57
  def record
51
58
  tracer = tracing.trace
@@ -66,16 +73,20 @@ module AppMap
66
73
  }
67
74
  end
68
75
 
69
- # class_map builds a class map from a config and a list of Ruby methods.
76
+ # Uploads an AppMap to the AppLand website and displays it.
77
+ def open(appmap = nil, &block)
78
+ appmap ||= AppMap.record(&block)
79
+ AppMap::Open.new(appmap).perform
80
+ end
81
+
82
+ # Builds a class map from a config and a list of Ruby methods.
70
83
  def class_map(methods)
71
- require 'appmap/class_map'
72
84
  ClassMap.build_from_methods(configuration, methods)
73
85
  end
74
86
 
75
- # detect_metadata returns default metadata detected from the Ruby system and from the
87
+ # Returns default metadata detected from the Ruby system and from the
76
88
  # filesystem.
77
89
  def detect_metadata
78
- require 'appmap/metadata'
79
90
  @metadata ||= Metadata.detect.freeze
80
91
  @metadata.deep_dup
81
92
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext'
4
-
5
3
  module AppMap
6
4
  class ClassMap
7
5
  module HasChildren
@@ -50,7 +48,7 @@ module AppMap
50
48
  end
51
49
  end
52
50
  Function = Struct.new(:name) do
53
- attr_accessor :static, :location
51
+ attr_accessor :static, :location, :labels
54
52
 
55
53
  def type
56
54
  'function'
@@ -61,8 +59,9 @@ module AppMap
61
59
  name: name,
62
60
  type: type,
63
61
  location: location,
64
- static: static
65
- }
62
+ static: static,
63
+ labels: labels
64
+ }.delete_if { |k,v| v.nil? || v == [] }
66
65
  end
67
66
  end
68
67
  end
@@ -71,35 +70,21 @@ module AppMap
71
70
  def build_from_methods(config, methods)
72
71
  root = Types::Root.new
73
72
  methods.each do |method|
74
- package = package_for_method(config.packages, method)
75
- add_function root, package.path, method
73
+ package = config.package_for_method(method) \
74
+ or raise "No package found for method #{method}"
75
+ add_function root, package, method
76
76
  end
77
77
  root.children.map(&:to_h)
78
78
  end
79
79
 
80
80
  protected
81
81
 
82
- def package_for_method(packages, method)
83
- location = method.method.source_location
84
- location_file, = location
85
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
86
-
87
- packages.find do |pkg|
88
- (location_file.index(pkg.path) == 0) &&
89
- !pkg.exclude.find { |p| location_file.index(p) }
90
- end or raise "No package found for method #{method}"
91
- end
92
-
93
- def add_function(root, package_name, method)
94
- location = method.method.source_location
95
- location_file, lineno = location
96
- location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
97
-
82
+ def add_function(root, package, method)
98
83
  static = method.static
99
84
 
100
85
  object_infos = [
101
86
  {
102
- name: package_name,
87
+ name: package.path,
103
88
  type: 'package'
104
89
  }
105
90
  ]
@@ -109,12 +94,25 @@ module AppMap
109
94
  type: 'class'
110
95
  }
111
96
  end
112
- object_infos << {
113
- name: method.method.name,
97
+ function_info = {
98
+ name: method.name,
114
99
  type: 'function',
115
- location: [ location_file, lineno ].join(':'),
116
100
  static: static
117
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
+
113
+ function_info[:labels] = package.labels if package.labels
114
+ object_infos << function_info
115
+
118
116
  parent = root
119
117
  object_infos.each do |info|
120
118
  parent = find_or_create parent.children, info do
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
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
18
+ end
19
+
20
+ Hook = Struct.new(:method_names, :package) do
21
+ end
22
+
23
+ OPENSSL_PACKAGE = Package.new('openssl', package_name: 'openssl', labels: %w[security crypto])
24
+
25
+ # Methods that should always be hooked, with their containing
26
+ # package and labels that should be applied to them.
27
+ HOOKED_METHODS = {
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
49
+
50
+ attr_reader :name, :packages
51
+
52
+ def initialize(name, packages = [])
53
+ @name = name
54
+ @packages = packages
55
+ end
56
+
57
+ class << self
58
+ # Loads configuration data from a file, specified by the file name.
59
+ def load_from_file(config_file_name)
60
+ require 'yaml'
61
+ load YAML.safe_load(::File.read(config_file_name))
62
+ end
63
+
64
+ # Loads configuration from a Hash.
65
+ def load(config_data)
66
+ packages = (config_data['packages'] || []).map do |package|
67
+ Package.new(package['path'], exclude: package['exclude'] || [])
68
+ end
69
+ Config.new config_data['name'], packages
70
+ end
71
+ end
72
+
73
+ def to_h
74
+ {
75
+ name: name,
76
+ packages: packages.map(&:to_h)
77
+ }
78
+ end
79
+
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
+
85
+ location = method.source_location
86
+ location_file, = location
87
+ return unless location_file
88
+
89
+ location_file = location_file[Dir.pwd.length + 1..-1] if location_file.index(Dir.pwd) == 0
90
+ packages.find do |pkg|
91
+ (location_file.index(pkg.path) == 0) &&
92
+ !pkg.exclude.find { |p| location_file.index(p) }
93
+ end
94
+ end
95
+
96
+ def included_by_location?(method)
97
+ !!package_for_method(method)
98
+ end
99
+
100
+ def always_hook?(defined_class, method_name)
101
+ !!find_package(defined_class, method_name)
102
+ end
103
+
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
109
+ end
110
+
111
+ def find_hook(defined_class)
112
+ HOOKED_METHODS[defined_class] || BUILTIN_METHODS[defined_class]
113
+ end
114
+ end
115
+ end