appmap 0.34.0 → 0.35.0

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: 47030df4910179ac636b8a2c4c3c29c25225deb1b9cbce851264d1e2ae8dacf1
4
- data.tar.gz: 84bc3de36a1f7700fe32128c40f8d2bf6d698208689e856fd2bc696c38be6500
3
+ metadata.gz: 587d9e4e90152c5ab6b04d4fa6b95d3071419563d82f4ed3c609def87111757b
4
+ data.tar.gz: b1867167f44a1f244e165a628293415dd54bcaacb0b4471c5d30c26dce3bef64
5
5
  SHA512:
6
- metadata.gz: 98d8933599f9cdb39d2ab9d69d1c11c7f06972603c30b5d79b3c57dc2ec246aec2c432da4835bcab805d49ec17e757fc0377d583908c6894380e951071b2d797
7
- data.tar.gz: eefe2496acc6be7ebfe20eee2ab9ed0e708658a039c196757dd00ca18351e0425d3a6d1f0599cca37bd4a2c786469d61fdc7ad0e50a56d9223360204e2bc4206
6
+ metadata.gz: 23a6e927bda905ce13666aa514040415bbaf0b8f317ffac524b1f0f94de58de17c175e001c921825ac0cbe05287a3e137899e5a5f86609cdbd9ce4000b4cbe4f
7
+ data.tar.gz: dcd2ad0a96709cc28f383726c56e133a75ba81e3edbd6679913afc7c1ae4097c3767c79b07ebf5bc9357c40660be00d39205dcbf4c1316a7add323b613a2f1a3
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,34 @@
1
+ # v0.35.0
2
+ * Provide a custom display string for files and HTTP requests.
3
+ * Report `mime_type` on HTTP response.
4
+
5
+ # v0.34.6
6
+ * Only warn once about problems determining database version for an ActiveRecord
7
+ connection.
8
+
9
+ # v0.34.5
10
+ * Ensure that hooking a method doesn't change its arity.
11
+
12
+ # v0.34.4
13
+ * Make sure `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.server_version` only calls
14
+ `ActiveRecord::Base.connection.database_version` if it's available.
15
+ * Fix `AppMap:Rails::SQLExaminer::ActiveRecordExaminer.database_type` returns `:postgres`
16
+ in all supported versions of Rails.
17
+
18
+ # v0.34.3
19
+ * Fix a crash in `singleton_method_owner_name` that occurred if `__attached__.class` returned
20
+ something other than a `Module` or a `Class`.
21
+
22
+ # v0.34.2
23
+ * Add an extension that gets the name of the owner of a singleton method without calling
24
+ any methods that may have been redefined (e.g. `#to_s` or `.name`).
25
+
26
+ # v0.34.1
27
+ * Ensure that capturing events doesn't change the behavior of a hooked method that uses
28
+ `Time.now`. For example, if a test expects that `Time.now` will be called a certain
29
+ number of times by a hooked method, that expectation will now be met.
30
+ * Make sure `appmap/cucumber` requires `appmap`.
31
+
1
32
  # v0.34.0
2
33
 
3
34
  * Records builtin security and I/O methods from `OpenSSL`, `Net`, and `IO`.
data/README.md CHANGED
@@ -57,6 +57,15 @@ end
57
57
 
58
58
  Then install with `bundle`.
59
59
 
60
+ **Railtie**
61
+
62
+ If you are using Ruby on Rails, require the railtie after Rails is loaded.
63
+
64
+ ```
65
+ # application.rb is a good place to do this, along with all the other railties.
66
+ require 'appmap/railtie'
67
+ ```
68
+
60
69
  # Configuration
61
70
 
62
71
  When you run your program, the `appmap` gem reads configuration settings from `appmap.yml`. Here's a sample configuration
@@ -267,7 +276,13 @@ $ bundle config local.appmap $(pwd)
267
276
  Run the tests via `rake`:
268
277
  ```
269
278
  $ bundle exec rake test
270
- ```
279
+ ```
280
+
281
+ The `test` target will build the native extension first, then run the tests. If you need
282
+ to build the extension separately, run
283
+ ```
284
+ $ bundle exec rake compile
285
+ ```
271
286
 
272
287
  ## Using fixture apps
273
288
 
@@ -286,7 +301,7 @@ resources such as a PostgreSQL database.
286
301
  To build the fixture container images, first run:
287
302
 
288
303
  ```sh-session
289
- $ bundle exec rake fixtures:all
304
+ $ bundle exec rake build:fixtures:all
290
305
  ```
291
306
 
292
307
  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']
@@ -34,6 +36,7 @@ Gem::Specification.new do |spec|
34
36
  spec.add_development_dependency 'rake', '>= 12.3.3'
35
37
  spec.add_development_dependency 'rdoc'
36
38
  spec.add_development_dependency 'rubocop'
39
+ spec.add_development_dependency "rake-compiler"
37
40
 
38
41
  # Testing
39
42
  spec.add_development_dependency 'climate_control'
@@ -42,4 +45,6 @@ Gem::Specification.new do |spec|
42
45
  spec.add_development_dependency 'rspec'
43
46
  spec.add_development_dependency 'selenium-webdriver'
44
47
  spec.add_development_dependency 'webdrivers', '~> 4.0'
48
+ spec.add_development_dependency 'timecop'
49
+ spec.add_development_dependency 'hashie'
45
50
  end
@@ -0,0 +1,95 @@
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
+ #define ARITIES_KEY "__arities__"
12
+
13
+ VALUE am_AppMapHook;
14
+
15
+ static VALUE
16
+ singleton_method_owner_name(VALUE klass, VALUE method)
17
+ {
18
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
19
+ VALUE attached = rb_ivar_get(owner, rb_intern("__attached__"));
20
+ if (!CLASS_OR_MODULE_P(attached)) {
21
+ attached = rb_funcall(attached, rb_intern("class"), 0);
22
+ }
23
+
24
+ // Did __attached__.class return an object that's a Module or a
25
+ // Class?
26
+ if (CLASS_OR_MODULE_P(attached)) {
27
+ // Yup, get it's name
28
+ return rb_mod_name(attached);
29
+ }
30
+
31
+ // Nope (which seems weird, but whatever). Fall back to calling
32
+ // #to_s on the method's owner and hope for the best.
33
+ return rb_funcall(owner, rb_intern("to_s"), 0);
34
+ }
35
+
36
+
37
+ static VALUE
38
+ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
39
+ {
40
+ VALUE arities_key = rb_intern(ARITIES_KEY);
41
+ VALUE arities = rb_ivar_get(mod, arities_key);
42
+
43
+ if (arities == Qundef || NIL_P(arities)) {
44
+ arities = rb_hash_new();
45
+ rb_ivar_set(mod, arities_key, arities);
46
+ }
47
+ rb_hash_aset(arities, name, arity);
48
+
49
+ return rb_funcall(mod, rb_intern("define_method"), 2, name, proc);
50
+ }
51
+
52
+ static VALUE
53
+ am_get_method_arity(VALUE method, VALUE orig_arity_method)
54
+ {
55
+ VALUE owner = rb_funcall(method, rb_intern("owner"), 0);
56
+ VALUE arities = rb_ivar_get(owner, rb_intern(ARITIES_KEY));
57
+ VALUE name = rb_funcall(method, rb_intern("name"), 0);
58
+ VALUE arity = Qnil;
59
+ // See if we saved an arity for the method.
60
+ if (!NIL_P(arities)) {
61
+ arity = rb_hash_aref(arities, name);
62
+ }
63
+ // Didn't find one, call the original method.
64
+ if (NIL_P(arity)) {
65
+ VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
+ arity = rb_funcall(bound_method, rb_intern("call"), 0);
67
+ }
68
+
69
+ return arity;
70
+ }
71
+
72
+ static VALUE
73
+ am_unbound_method_arity(VALUE method)
74
+ {
75
+ VALUE orig_unbound_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@unbound_method_arity"));
76
+ return am_get_method_arity(method, orig_unbound_method_arity);
77
+ }
78
+
79
+ static VALUE
80
+ am_method_arity(VALUE method)
81
+ {
82
+ VALUE orig_method_arity = rb_ivar_get(am_AppMapHook, rb_intern("@method_arity"));
83
+ return am_get_method_arity(method, orig_method_arity);
84
+ }
85
+
86
+ void Init_appmap() {
87
+ VALUE appmap = rb_define_module("AppMap");
88
+ am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
89
+
90
+ rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
91
+
92
+ rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
93
+ rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
94
+ rb_define_method(rb_cMethod, "arity", am_method_arity, 0);
95
+ }
@@ -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))
@@ -16,6 +16,9 @@ require 'appmap/metadata'
16
16
  require 'appmap/util'
17
17
  require 'appmap/open'
18
18
 
19
+ # load extension
20
+ require 'appmap/appmap'
21
+
19
22
  module AppMap
20
23
  class << self
21
24
  @configuration = nil
@@ -81,7 +84,7 @@ module AppMap
81
84
 
82
85
  # Builds a class map from a config and a list of Ruby methods.
83
86
  def class_map(methods)
84
- ClassMap.build_from_methods(configuration, methods)
87
+ ClassMap.build_from_methods(methods)
85
88
  end
86
89
 
87
90
  # Returns default metadata detected from the Ruby system and from the
@@ -61,25 +61,24 @@ module AppMap
61
61
  location: location,
62
62
  static: static,
63
63
  labels: labels
64
- }.delete_if { |k,v| v.nil? || v == [] }
64
+ }.delete_if { |_, v| v.nil? || v == [] }
65
65
  end
66
66
  end
67
67
  end
68
68
 
69
69
  class << self
70
- def build_from_methods(config, methods)
70
+ def build_from_methods(methods)
71
71
  root = Types::Root.new
72
72
  methods.each do |method|
73
- package = config.package_for_method(method) \
74
- or raise "No package found for method #{method}"
75
- add_function root, package, method
73
+ add_function root, method
76
74
  end
77
75
  root.children.map(&:to_h)
78
76
  end
79
77
 
80
78
  protected
81
79
 
82
- def add_function(root, package, method)
80
+ def add_function(root, method)
81
+ package = method.package
83
82
  static = method.static
84
83
 
85
84
  object_infos = [
@@ -6,7 +6,7 @@ module AppMap
6
6
  def initialize(path, package_name: nil, exclude: [], labels: [])
7
7
  super path, package_name, exclude, labels
8
8
  end
9
-
9
+
10
10
  def to_h
11
11
  {
12
12
  path: path,
@@ -105,7 +105,7 @@ module AppMap
105
105
  hook = find_hook(defined_class)
106
106
  return nil unless hook
107
107
 
108
- Array(hook.method_names).include?(method_name) ? hook.package : nil
108
+ Array(hook.method_names).include?(method_name) ? hook.package : nil
109
109
  end
110
110
 
111
111
  def find_hook(defined_class)
@@ -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
@@ -36,20 +36,38 @@ module AppMap
36
36
  '*Error inspecting variable*'
37
37
  end
38
38
 
39
- value_string = \
39
+ value_string = custom_display_string(value) || default_display_string(value)
40
+
41
+ (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
42
+ end
43
+
44
+ protected
45
+
46
+ def custom_display_string(value)
47
+ case value
48
+ when File
49
+ "#{value.class}[path=#{value.path}]"
50
+ when Net::HTTP
51
+ "#{value.class}[#{value.address}:#{value.port}]"
52
+ when Net::HTTPGenericRequest
53
+ "#{value.class}[#{value.method} #{value.path}]"
54
+ end
55
+ rescue StandardError
56
+ nil
57
+ end
58
+
59
+ def default_display_string(value)
60
+ begin
61
+ value.to_s
62
+ rescue NoMethodError
40
63
  begin
41
- value.to_s
42
- rescue NoMethodError
43
- begin
44
- value.inspect
45
- rescue StandardError
46
- last_resort_string.call
47
- end
64
+ value.inspect
48
65
  rescue StandardError
49
66
  last_resort_string.call
50
67
  end
51
-
52
- (value_string||'')[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
68
+ rescue StandardError
69
+ last_resort_string.call
70
+ end
53
71
  end
54
72
  end
55
73
  end
@@ -6,6 +6,9 @@ module AppMap
6
6
  class Hook
7
7
  LOG = (ENV['DEBUG'] == 'true')
8
8
 
9
+ @unbound_method_arity = ::UnboundMethod.instance_method(:arity)
10
+ @method_arity = ::Method.instance_method(:arity)
11
+
9
12
  class << self
10
13
  def lock_builtins
11
14
  return if @builtins_hooked
@@ -17,18 +20,7 @@ module AppMap
17
20
  # the given method.
18
21
  def qualify_method_name(method)
19
22
  if method.owner.singleton_class?
20
- # Singleton class names can take two forms:
21
- # #<Class:Foo> or
22
- # #<Class:#<Bar:0x0123ABC>>. Retrieve the name of
23
- # the class from the string.
24
- #
25
- # (There really isn't a better way to do this. The
26
- # singleton's reference to the class it was created
27
- # from is stored in an instance variable named
28
- # '__attached__'. It doesn't have the '@' prefix, so
29
- # it's internal only, and not accessible from user
30
- # code.)
31
- class_name = /#<Class:((#<(?<cls>.*?):)|((?<cls>.*?)>))/.match(method.owner.to_s)['cls']
23
+ class_name = singleton_method_owner_name(method)
32
24
  [ class_name, '.', method.name ]
33
25
  else
34
26
  [ method.owner.name, '#', method.name ]
@@ -56,22 +48,23 @@ module AppMap
56
48
  hook = lambda do |hook_cls|
57
49
  lambda do |method_id|
58
50
  method = hook_cls.public_instance_method(method_id)
59
- hook_method = Hook::Method.new(hook_cls, method)
60
51
 
61
- warn "AppMap: Examining #{hook_method.method_display_name}" if LOG
52
+ warn "AppMap: Examining #{hook_cls} #{method.name}" if LOG
62
53
 
63
54
  disasm = RubyVM::InstructionSequence.disasm(method)
64
55
  # Skip methods that have no instruction sequence, as they are obviously trivial.
65
56
  next unless disasm
66
57
 
58
+ next unless \
59
+ config.always_hook?(hook_cls, method.name) ||
60
+ config.included_by_location?(method)
61
+
62
+ hook_method = Hook::Method.new(config.package_for_method(method), hook_cls, method)
63
+
67
64
  # Don't try and trace the AppMap methods or there will be
68
65
  # a stack overflow in the defined hook method.
69
66
  next if /\AAppMap[:\.]/.match?(hook_method.method_display_name)
70
67
 
71
- next unless \
72
- config.always_hook?(hook_method.defined_class, method.name) ||
73
- config.included_by_location?(method)
74
-
75
68
  hook_method.activate
76
69
  end
77
70
  end
@@ -105,9 +98,9 @@ module AppMap
105
98
  end
106
99
 
107
100
  if method
108
- Hook::Method.new(cls, method).activate
101
+ Hook::Method.new(hook.package, cls, method).activate
109
102
  else
110
- warn "Method #{method_name} not found on #{cls.name}"
103
+ warn "Method #{method_name} not found on #{cls.name}"
111
104
  end
112
105
  end
113
106
  end