appmap 0.28.1 → 0.34.1

Sign up to get free protection for your applications and to get access to all the features.
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