appmap 0.77.3 → 0.79.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.travis.yml +4 -23
  4. data/CHANGELOG.md +21 -0
  5. data/{spec/fixtures/rails5_users_app/Dockerfile.pg → Dockerfile.pg} +0 -0
  6. data/README.md +14 -44
  7. data/README_CI.md +0 -7
  8. data/Rakefile +12 -150
  9. data/appmap.gemspec +3 -2
  10. data/docker-compose.yml +10 -0
  11. data/ext/appmap/appmap.c +21 -2
  12. data/lib/appmap/builtin_hooks/ruby.yml +6 -3
  13. data/lib/appmap/handler/eval.rb +41 -0
  14. data/lib/appmap/handler/function.rb +8 -8
  15. data/lib/appmap/handler/net_http.rb +19 -18
  16. data/lib/appmap/handler/rails/request_handler.rb +3 -4
  17. data/lib/appmap/handler/rails/template.rb +68 -62
  18. data/lib/appmap/hook/method/ruby2.rb +56 -0
  19. data/lib/appmap/hook/method/ruby3.rb +56 -0
  20. data/lib/appmap/hook/method.rb +42 -98
  21. data/lib/appmap/hook.rb +2 -2
  22. data/lib/appmap/version.rb +1 -1
  23. data/spec/config_spec.rb +1 -1
  24. data/spec/depends/api_spec.rb +13 -5
  25. data/spec/depends/spec_helper.rb +0 -9
  26. data/spec/fixtures/database.yml +11 -0
  27. data/spec/fixtures/rails5_users_app/config/database.yml +1 -0
  28. data/spec/fixtures/rails6_users_app/Gemfile +1 -25
  29. data/spec/fixtures/rails6_users_app/config/database.yml +1 -0
  30. data/spec/fixtures/rails7_users_app/Gemfile +1 -25
  31. data/spec/fixtures/rails7_users_app/config/database.yml +1 -0
  32. data/spec/handler/eval_spec.rb +66 -0
  33. data/spec/hook_spec.rb +3 -3
  34. data/spec/rails_recording_spec.rb +4 -20
  35. data/spec/rails_spec_helper.rb +76 -63
  36. data/spec/rails_test_spec.rb +7 -17
  37. data/spec/railtie_spec.rb +4 -18
  38. data/spec/record_sql_rails_pg_spec.rb +44 -75
  39. data/spec/remote_recording_spec.rb +18 -30
  40. data/spec/spec_helper.rb +1 -0
  41. data/spec/swagger/swagger_spec.rb +1 -16
  42. data/spec/util_spec.rb +1 -1
  43. metadata +25 -21
  44. data/Dockerfile.appmap +0 -5
  45. data/spec/fixtures/rack_users_app/Dockerfile +0 -32
  46. data/spec/fixtures/rack_users_app/docker-compose.yml +0 -9
  47. data/spec/fixtures/rails5_users_app/Dockerfile +0 -29
  48. data/spec/fixtures/rails5_users_app/config/database.yml +0 -18
  49. data/spec/fixtures/rails5_users_app/create_app +0 -33
  50. data/spec/fixtures/rails5_users_app/docker-compose.yml +0 -31
  51. data/spec/fixtures/rails6_users_app/.ruby-version +0 -1
  52. data/spec/fixtures/rails6_users_app/Dockerfile +0 -44
  53. data/spec/fixtures/rails6_users_app/Dockerfile.pg +0 -3
  54. data/spec/fixtures/rails6_users_app/config/database.yml +0 -18
  55. data/spec/fixtures/rails6_users_app/create_app +0 -33
  56. data/spec/fixtures/rails6_users_app/docker-compose.yml +0 -31
  57. data/spec/fixtures/rails7_users_app/.ruby-version +0 -1
  58. data/spec/fixtures/rails7_users_app/Dockerfile +0 -30
  59. data/spec/fixtures/rails7_users_app/Dockerfile.pg +0 -3
  60. data/spec/fixtures/rails7_users_app/config/database.yml +0 -86
  61. data/spec/fixtures/rails7_users_app/create_app +0 -31
  62. data/spec/fixtures/rails7_users_app/docker-compose.yml +0 -31
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6889544929fdf05360b3e528847aaa1e3c4a423c580b982d5ecbb3403a44f6aa
4
- data.tar.gz: dad6b337385f4700b0974bf727329afa2bcd4c33f5a17bd951fd9516608a5798
3
+ metadata.gz: f5c6a40273c5606504492f151b2feeb1addc83d92f6bec7a80ca51d3034cd79f
4
+ data.tar.gz: b10a582926396930d2a980cd4323bd83f7c3444aa0b77bf2dd2dfa632203f65d
5
5
  SHA512:
6
- metadata.gz: 197d249f8aa3ca22766168688f3b31e3b9cdf4c16d20227012d77dc5d6d1eb16b7814af8e605b51a5dc5a60ef4861f8cfd3f7089878ff88b046fac59a15b52a3
7
- data.tar.gz: 0e74d7f95043ea588f0a0ef9aca170765db7475c6009d6471ce2299885b59aca25eb4d87bb72cd7045a2a743b6b8c95fbb6348786d99cb938a269c73189a5234
6
+ metadata.gz: f56a49c05561a74ae2ee7512fbad2beadc1a0aa719e27b455ea330777c58e97b7c028f7eaaee13d52f20c8ace6bf9284ff8c717e17d922a45f2dca703781a737
7
+ data.tar.gz: d35ac177b2b12ddd4f4e0517ed4101043c8e9d29f89f53721969d45e3e2be293ae7ad0b8466a2fae63329bb7a13e27d699b30a2464a663014d04a71a8d464293
data/.rubocop.yml CHANGED
@@ -19,6 +19,7 @@ Layout/LineLength:
19
19
 
20
20
  Metrics/BlockLength:
21
21
  ExcludedMethods:
22
+ - describe
22
23
  - it
23
24
  - context
24
25
 
data/.travis.yml CHANGED
@@ -1,5 +1,8 @@
1
1
  language: ruby
2
2
  dist: bionic
3
+ cache:
4
+ - bundle
5
+ - yarn
3
6
 
4
7
  # NB: if you update the ruby versions here, you also need to update
5
8
  # them in the Rakefile.
@@ -9,37 +12,15 @@ rbenv:
9
12
  - 3.0
10
13
  - 3.1
11
14
 
12
- addons:
13
- apt:
14
- packages:
15
- # https://docs.travis-ci.com/user/docker/#installing-a-newer-docker-version
16
- - docker-ce
17
-
18
15
  services:
19
- - docker
16
+ - postgresql
20
17
 
21
18
  before_install:
22
- - sudo apt-get update && sudo apt-get install apt-transport-https ca-certificates -y && sudo update-ca-certificates
23
- # see https://blog.travis-ci.com/docker-rate-limits
24
- # and also https://www.docker.com/blog/what-you-need-to-know-about-upcoming-docker-hub-rate-limiting/
25
- # if we do not use authorized account,
26
- # the pulls-per-IP quota is shared with other Travis users
27
- - >
28
- if [ ! -z "$DOCKERHUB_PASSWORD" ] && [ ! -z "$DOCKERHUB_USERNAME" ]; then
29
- echo "$DOCKERHUB_PASSWORD" | docker login -u "$DOCKERHUB_USERNAME" --password-stdin ;
30
- fi
31
19
  - |
32
20
  nvm install --lts \
33
21
  && nvm use --lts \
34
22
  && npm i -g yarn
35
23
 
36
- jobs:
37
- include:
38
- - stage: test
39
- env:
40
- # GEM_ALTERNATIVE_NAME only needed for deployment
41
- - GEM_ALTERNATIVE_NAME=''
42
-
43
24
  before_deploy:
44
25
  - |
45
26
  npm i -g \
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [0.79.0](https://github.com/applandinc/appmap-ruby/compare/v0.78.0...v0.79.0) (2022-04-06)
2
+
3
+
4
+ ### Features
5
+
6
+ * Use a more unique test database name ([0eed036](https://github.com/applandinc/appmap-ruby/commit/0eed036460f0384698ff91c1112a4a9c3214f7f4))
7
+
8
+ # [0.78.0](https://github.com/applandinc/appmap-ruby/compare/v0.77.4...v0.78.0) (2022-04-04)
9
+
10
+
11
+ ### Features
12
+
13
+ * Hook and label Kernel#eval ([e0c151d](https://github.com/applandinc/appmap-ruby/commit/e0c151d1371f5bed5597ffd0d3bfebb8bca247c2))
14
+
15
+ ## [0.77.4](https://github.com/applandinc/appmap-ruby/compare/v0.77.3...v0.77.4) (2022-04-04)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * Update Rails request handler to the new hook architecture ([595b39a](https://github.com/applandinc/appmap-ruby/commit/595b39abb030c1dcf85c83e4717c25d4c5177d4d))
21
+
1
22
  ## [0.77.3](https://github.com/applandinc/appmap-ruby/compare/v0.77.2...v0.77.3) (2022-03-29)
2
23
 
3
24
 
data/README.md CHANGED
@@ -28,7 +28,7 @@ Visit the [AppMap for Ruby](https://appland.com/docs/reference/appmap-ruby.html)
28
28
 
29
29
  **Configuration**
30
30
 
31
- *appmap.yml* is loaded into an `AppMap::Config`.
31
+ *appmap.yml* is loaded into an `AppMap::Config`.
32
32
 
33
33
  **Hooking**
34
34
 
@@ -39,7 +39,7 @@ method with calls that record the parameters and return value.
39
39
  **Builtins**
40
40
 
41
41
  `Hook` begins by iterating over builtin classes and modules defined in the `Config`. Builtins include code
42
- like `openssl` and `net/http`. This code is not dependent on any external libraries being present, and
42
+ like `openssl` and `net/http`. This code is not dependent on any external libraries being present, and
43
43
  `appmap` cannot guarantee that it will be loaded before builtins. Therefore, it's necessary to require it and
44
44
  hook it by looking up the classes and modules as constants in the `Object` namespace.
45
45
 
@@ -81,50 +81,20 @@ The fixture apps in `test/fixtures` are plain Ruby projects that exercise the ba
81
81
 
82
82
  The fixture apps in `spec/fixtures` are simple Rack, Rails5, and Rails6 apps.
83
83
  You can use them to interactively develop and test the recording features of the `appmap` gem.
84
- These fixture apps are more sophisticated than `test/fixtures`, because they include additional
85
- resources such as a PostgreSQL database.
84
+ These fixture apps are more sophisticated than `test/fixtures`, because they include additional
85
+ resources such as a PostgreSQL database. Still, you can simply enter the fixture directory and `bundle`.
86
86
 
87
- To build the fixture container images, first run:
87
+ If you don't have PostgreSQL on the local (default) socket, you can export `DATABASE_URL` to
88
+ point to the database server you want to use.
88
89
 
89
- ```sh-session
90
- $ bundle exec rake build:fixtures:all
91
- ```
92
-
93
- This will build the `appmap.gem`, along with a Docker image for each fixture app.
94
-
95
- Then move to the directory of the fixture you want to use, and provision the environment.
96
- In this example, we use Ruby 2.6.
97
-
98
- ```sh-session
99
- $ export RUBY_VERSION=2.6
100
- $ docker-compose up -d pg
101
- $ sleep 10s # Or some reasonable amount of time
102
- $ docker-compose run --rm app ./create_app
103
- ```
90
+ You can launch a database like this:
104
91
 
105
- Now you can start a development container.
106
-
107
- ```sh-session
108
- $ docker-compose run --rm -v $PWD:/app -v $PWD/../../..:/src/appmap-ruby app bash
109
- Starting rails_users_app_pg_1 ... done
110
- root@6fab5f89125f:/app# cd /src/appmap-ruby
111
- root@6fab5f89125f:/src/appmap-ruby# rm ext/appmap/*.so ext/appmap/*.o
112
- root@6fab5f89125f:/src/appmap-ruby# bundle
113
- root@6fab5f89125f:/src/appmap-ruby# bundle exec rake compile
114
- root@6fab5f89125f:/src/appmap-ruby# cd /src/app
115
- root@6fab5f89125f:/src/app# bundle config local.appmap /src/appmap-ruby
116
- root@6fab5f89125f:/src/app# bundle
117
92
  ```
118
-
119
- At this point, the bundle is built with the `appmap` gem located in `/src/appmap`, which is volume-mounted from the host.
120
- So you can edit the fixture code and the appmap code and run test commands such as `rspec` in the container.
121
- For example:
122
-
123
- ```sh-session
124
- root@6fab5f89125f:/src/app# APPMAP=true bundle exec rspec
125
- Configuring AppMap from path appmap.yml
126
- ....
127
-
128
- Finished in 0.07357 seconds (files took 2.1 seconds to load)
129
- 4 examples, 0 failures
93
+ ➜ docker-compose -p appmap-ruby up -d
94
+ ... stuff
95
+ docker-compose ps pg
96
+ Name Command State Ports
97
+ -----------------------------------------------------------------------------------------
98
+ appmap-ruby_pg_1 docker-entrypoint.sh postgres Up (healthy) 0.0.0.0:59134->5432/tcp
99
+ export DATABASE_URL=postgres://postgres@localhost:59134
130
100
  ```
data/README_CI.md CHANGED
@@ -4,13 +4,6 @@
4
4
  * `GEM_HOST_API_KEY`: rubygems API key
5
5
  * `GEM_ALTERNATIVE_NAME` (optional): used for testing of CI flows,
6
6
  to avoid publication of test releases under official package name
7
- * `DOCKERHUB\_USERNAME`, `DOCKERHUB_PASSWORD`: optional dockerhub credentials,
8
- to avoid throttling of dockerhub anonymous pulls
9
-
10
- Note: for security reasons, it's better to use dedicated (not personal)
11
- Dockerhub account,
12
- and also use [access tokens](https://docs.docker.com/docker-hub/access-tokens/)
13
- instead of primary password
14
7
 
15
8
  # Release command
16
9
 
data/Rakefile CHANGED
@@ -1,26 +1,8 @@
1
1
  $: << File.join(__dir__, 'lib')
2
- require 'appmap/version'
3
- GEM_VERSION = AppMap::VERSION
4
-
5
- # Make sure the local version is not behind the one on
6
- # rubygems.org (it's ok if they're the same).
7
- #
8
- # If it is behind, the fixture images won't get updated with the gem
9
- # built from the local source, so you'll wind up testing the rubygems
10
- # version instead.
11
- unless ENV['SKIP_VERSION_CHECK']
12
- require 'json'
13
- require 'net/http'
14
- rubygems_version = JSON.parse(Net::HTTP.get(URI.parse('https://rubygems.org/api/v1/gems/appmap.json')))['version']
15
- if Gem::Version.new(GEM_VERSION) < Gem::Version.new(rubygems_version)
16
- raise "#{GEM_VERSION} < #{rubygems_version}. Rebase to avoid build issues."
17
- end
18
- end
19
2
 
3
+ require 'rspec/core/rake_task'
20
4
  require 'rake/testtask'
21
5
  require 'rdoc/task'
22
-
23
- require 'open3'
24
6
  require 'rake/extensiontask'
25
7
 
26
8
  desc 'build the native extension'
@@ -28,138 +10,20 @@ Rake::ExtensionTask.new("appmap") do |ext|
28
10
  ext.lib_dir = "lib/appmap"
29
11
  end
30
12
 
31
- RUBY_VERSIONS=%w[2.6 2.7 3.0 3.1].select do |version|
32
- travis_ruby_version = ENV['TRAVIS_RUBY_VERSION']
33
- next true unless travis_ruby_version
34
-
35
- if travis_ruby_version.index(version) == 0
36
- warn "Testing Ruby version #{version}, since it matches TRAVIS_RUBY_VERSION=#{travis_ruby_version}"
37
- next true
38
- end
39
-
40
- false
41
- end
42
- FIXTURE_APPS=[:rack_users_app, :rails6_users_app, :rails5_users_app, :rails7_users_app => {:ruby_version => '>= 2.7'}]
43
-
44
- def run_cmd(*cmd)
45
- $stderr.puts "Running: #{cmd}"
46
- out, s = Open3.capture2e(*cmd)
47
- unless s.success?
48
- $stderr.puts <<-END
49
- Command failed:
50
- <<< Output:
51
- #{out}
52
- >>> End of output
53
- END
54
- raise 'Docker build failed'
55
- end
56
- end
57
-
58
- def build_base_image(ruby_version)
59
- run_cmd "docker build" \
60
- " --build-arg RUBY_VERSION=#{ruby_version}" \
61
- " --build-arg GEM_VERSION=#{GEM_VERSION}" \
62
- " -t appmap:#{GEM_VERSION} -f Dockerfile.appmap ."
63
- end
64
-
65
- def build_app_image(app, ruby_version)
66
- Dir.chdir "spec/fixtures/#{app}" do
67
- env = {"RUBY_VERSION" => ruby_version, "GEM_VERSION" => GEM_VERSION}
68
- run_cmd(env,
69
- "docker-compose build" \
70
- " --build-arg RUBY_VERSION=#{ruby_version}" \
71
- " --build-arg GEM_VERSION=#{GEM_VERSION}" )
72
- end
73
- end
74
-
75
13
  desc 'Install non-Ruby dependencies'
76
14
  task :install do
77
15
  system 'yarn install' or raise 'yarn install failed'
78
16
  end
79
17
 
80
- namespace :build do
81
- namespace :base do
82
- RUBY_VERSIONS.each do |ruby_version|
83
- desc ruby_version
84
- task ruby_version do
85
- run_system = ->(cmd) { system(cmd) or raise "Command failed: #{cmd}" }
86
-
87
- run_system.call 'mkdir -p pkg'
88
- run_system.call "gem build appmap.gemspec --output pkg/appmap-#{GEM_VERSION}.gem"
89
- build_base_image(ruby_version)
90
- end.tap do |t|
91
- desc "Build all images"
92
- task all: t
93
- end
94
- end
95
- end
96
-
97
- namespace :fixtures do
98
- RUBY_VERSIONS.each do |ruby_version|
99
- namespace ruby_version do
100
- desc "build:fixtures:#{ruby_version}"
101
- FIXTURE_APPS.each do |app_spec|
102
- app = if app_spec.instance_of?(Hash)
103
- app_spec = app_spec.flatten
104
- version_rqt = Gem::Requirement.create(app_spec[1][:ruby_version])
105
- next unless version_rqt =~ (Gem::Version.new(ruby_version))
106
- app = app_spec[0]
107
- else
108
- app = app_spec
109
- end.to_s
110
-
111
-
112
- desc app
113
- task app => ["base:#{ruby_version}"] do
114
- build_app_image(app, ruby_version)
115
- end.tap do |t|
116
- desc "Build all fixture images for #{ruby_version}"
117
- task all: t
118
- end
119
- end
120
- end
121
-
122
- desc "Build all fixture images"
123
- task all: ["#{ruby_version}:all"]
124
- end
125
- end
126
-
127
- task all: ["fixtures:all"]
128
- end
129
-
130
- def run_specs(ruby_version, task_args)
131
- require 'rspec/core/rake_task'
132
- require 'climate_control'
133
- # Define an rspec rake task for the specified Ruby version. It's hidden (i.e. doesn't have a
134
- # description), because it's not intended to be invoked directly
135
- RSpec::Core::RakeTask.new("rspec_#{ruby_version}", [:specs]) do |task, args|
136
- task.exclude_pattern = 'spec/fixtures/**/*_spec.rb'
137
- task.rspec_opts = '-f doc'
138
- if args.count > 0
139
- # There doesn't appear to be a value for +pattern+ that will
140
- # cause it to be ignored. Setting it to '' or +nil+ causes an
141
- # empty argument to get passed to rspec, which confuses it.
142
- task.pattern = 'never match this'
143
- task.rspec_opts += " " + args.to_a.join(' ')
144
- end
145
- end
146
-
147
- # Set up the environment, then execute the rspec task we
148
- # created above.
149
- ClimateControl.modify(RUBY_VERSION: ruby_version) do
150
- Rake::Task["rspec_#{ruby_version}"].execute(task_args)
151
- end
152
- end
153
-
154
- namespace :spec do
155
- RUBY_VERSIONS.each do |ruby_version|
156
- desc ruby_version
157
- task ruby_version, [:specs] => ["install", "compile", "build:fixtures:#{ruby_version}:all"] do |_, task_args|
158
- run_specs(ruby_version, task_args)
159
- end.tap do |t|
160
- desc "Run all specs"
161
- task :all, [:specs] => t
162
- end
18
+ RSpec::Core::RakeTask.new spec: %i[compile install] do |task, args|
19
+ task.exclude_pattern = 'spec/fixtures/**/*_spec.rb'
20
+ task.rspec_opts = '-f doc'
21
+ if args.count > 0
22
+ # There doesn't appear to be a value for +pattern+ that will
23
+ # cause it to be ignored. Setting it to '' or +nil+ causes an
24
+ # empty argument to get passed to rspec, which confuses it.
25
+ task.pattern = 'never match this'
26
+ task.rspec_opts += [nil, *args].join(' ')
163
27
  end
164
28
  end
165
29
 
@@ -169,14 +33,12 @@ Rake::RDocTask.new do |rd|
169
33
  rd.title = 'AppMap'
170
34
  end
171
35
 
172
- Rake::TestTask.new(minitest: 'compile') do |t|
36
+ Rake::TestTask.new(minitest: :compile) do |t|
173
37
  t.libs << 'test'
174
38
  t.libs << 'lib'
175
39
  t.test_files = FileList['test/*_test.rb']
176
40
  end
177
41
 
178
- task spec: %i[spec:all]
179
-
180
- task test: %i[spec:all minitest]
42
+ task test: %i[spec minitest]
181
43
 
182
44
  task default: :test
data/appmap.gemspec CHANGED
@@ -5,7 +5,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'appmap/version'
6
6
 
7
7
  Gem::Specification.new do |spec|
8
- # ability to parameterize gem name is added intentionally,
8
+ # ability to parameterize gem name is added intentionally,
9
9
  # to support the possibility of unofficial releases, e.g. during CI tests
10
10
  spec.name = (ENV['GEM_ALTERNATIVE_NAME'].to_s.empty? ? 'appmap' : ENV["GEM_ALTERNATIVE_NAME"] )
11
11
  spec.version = AppMap::VERSION
@@ -32,7 +32,7 @@ Gem::Specification.new do |spec|
32
32
  spec.add_dependency 'reverse_markdown'
33
33
 
34
34
  spec.add_development_dependency 'bundler', '>= 1.16'
35
- spec.add_development_dependency 'minitest', '~> 5.14'
35
+ spec.add_development_dependency 'minitest', '~> 5.15'
36
36
  spec.add_development_dependency 'pry-byebug'
37
37
  spec.add_development_dependency 'rake', '>= 12.3.3'
38
38
  spec.add_development_dependency 'rdoc'
@@ -48,5 +48,6 @@ Gem::Specification.new do |spec|
48
48
  spec.add_development_dependency 'webdrivers', '~> 4.0'
49
49
  spec.add_development_dependency 'timecop'
50
50
  spec.add_development_dependency 'hashie'
51
+ spec.add_development_dependency 'random-port', '~> 0.5.1'
51
52
  spec.add_development_dependency 'webrick'
52
53
  end
@@ -0,0 +1,10 @@
1
+ version: "3"
2
+ services:
3
+ pg:
4
+ build:
5
+ context: .
6
+ dockerfile: Dockerfile.pg
7
+ ports:
8
+ - "5432"
9
+ environment:
10
+ POSTGRES_HOST_AUTH_METHOD: trust
data/ext/appmap/appmap.c CHANGED
@@ -1,4 +1,5 @@
1
1
  #include <ruby.h>
2
+ #include <ruby/debug.h>
2
3
  #include <ruby/intern.h>
3
4
 
4
5
  // Seems like CLASS_OR_MODULE_P should really be in a header file in
@@ -39,7 +40,7 @@ am_define_method_with_arity(VALUE mod, VALUE name, VALUE arity, VALUE proc)
39
40
  {
40
41
  VALUE arities_key = rb_intern(ARITIES_KEY);
41
42
  VALUE arities = rb_ivar_get(mod, arities_key);
42
-
43
+
43
44
  if (arities == Qundef || NIL_P(arities)) {
44
45
  arities = rb_hash_new();
45
46
  rb_ivar_set(mod, arities_key, arities);
@@ -62,7 +63,7 @@ am_get_method_arity(VALUE method, VALUE orig_arity_method)
62
63
  }
63
64
  // Didn't find one, call the original method.
64
65
  if (NIL_P(arity)) {
65
- VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
+ VALUE bound_method = rb_funcall(orig_arity_method, rb_intern("bind"), 1, method);
66
67
  arity = rb_funcall(bound_method, rb_intern("call"), 0);
67
68
  }
68
69
 
@@ -83,11 +84,29 @@ am_method_arity(VALUE method)
83
84
  return am_get_method_arity(method, orig_method_arity);
84
85
  }
85
86
 
87
+ static VALUE
88
+ bindings_callback(const rb_debug_inspector_t *dbg_context, void *level)
89
+ {
90
+ const int i = NUM2INT((VALUE) level);
91
+ if (i >= RARRAY_LEN(rb_debug_inspector_backtrace_locations(dbg_context))) {
92
+ return Qnil;
93
+ } else {
94
+ return rb_debug_inspector_frame_binding_get(dbg_context, i);
95
+ }
96
+ }
97
+
98
+ static VALUE
99
+ am_previous_bindings(VALUE self, VALUE level)
100
+ {
101
+ return rb_debug_inspector_open(bindings_callback, (void *) level);
102
+ }
103
+
86
104
  void Init_appmap() {
87
105
  VALUE appmap = rb_define_module("AppMap");
88
106
  am_AppMapHook = rb_define_class_under(appmap, "Hook", rb_cObject);
89
107
 
90
108
  rb_define_singleton_method(am_AppMapHook, "singleton_method_owner_name", singleton_method_owner_name, 1);
109
+ rb_define_module_function(appmap, "caller_binding", am_previous_bindings, 1);
91
110
 
92
111
  rb_define_method(rb_cModule, "define_method_with_arity", am_define_method_with_arity, 3);
93
112
  rb_define_method(rb_cUnboundMethod, "arity", am_unbound_method_arity, 0);
@@ -14,14 +14,17 @@
14
14
  - String#unpack1
15
15
  require_name: ruby
16
16
  label: string.unpack
17
- #- methods:
17
+ - methods:
18
+ - Kernel#eval
19
+ require_name: ruby
20
+ label: lang.eval
21
+ handler_class: AppMap::Handler::Eval
18
22
  # TODO: eval does not happen in the right context, and therefore any new constants
19
23
  # which are defined are placed on the wrong module/class.
20
- # - Kernel#eval
21
24
  # - Binding#eval
22
25
  # - BasicObject#instance_eval
23
26
  # These methods cannot be hooked as far as I can tell.
24
- # Why? When calling one of these functions, the context at the point of
27
+ # Why? When calling one of these functions, the context at the point of
25
28
  # definition is used. It's not possible to bind class_eval to a new context.
26
29
  # - Module#class_eval
27
30
  # - Module#module_eval
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'appmap/handler/function'
4
+
5
+ module AppMap
6
+ module Handler
7
+ # Handler class for Kernel#eval.
8
+ #
9
+ # Second argument to eval is a Binding, which despite the name (and
10
+ # the accessible methods) in addition to locals and receiver also
11
+ # encapsulates the entire execution context, in particular including
12
+ # the lexical scope. This is especially important for constant lookup
13
+ # and definition.
14
+ #
15
+ # If the binding is not provided, by default eval will run in the
16
+ # current frame. Since we call it here, this will mean the #do_call
17
+ # frame, which would make AppMap::Handler::Eval the lexical scope
18
+ # for constant lookup and definition; as a consequence
19
+ # eg. `eval "class Foo; end"` would define
20
+ # AppMap::Handler::Eval::Foo instead of defining it in
21
+ # the module where the original call was made.
22
+ #
23
+ # To avoid this, we explicitly substitute the correct execution
24
+ # context, up several stack frames.
25
+ class Eval < Function
26
+ # The depth of the frame we need to pluck out:
27
+ # 1. Hook::Method#do_call
28
+ # 2. Hook::Method#trace_call
29
+ # 3. Hook::Method#call
30
+ # 4. proc generated by Hook::Method#hook_method_def
31
+ # 5. the (intended) frame of the original eval that we hooked
32
+ # Note it needs to be adjusted if this call sequence changes.
33
+ FRAME_DEPTH = 5
34
+
35
+ def do_call(receiver, src = nil, context = nil, *rest)
36
+ context ||= AppMap.caller_binding FRAME_DEPTH
37
+ hook_method.bind(receiver).call(src, context, *rest)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,18 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'appmap/event'
4
+ require 'appmap/hook/method'
4
5
 
5
6
  module AppMap
6
7
  module Handler
7
- module Function
8
- class << self
9
- def handle_call(defined_class, hook_method, receiver, args)
10
- AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
11
- end
8
+ # Base handler class, will emit method call and return events.
9
+ class Function < Hook::Method
10
+ def handle_call(receiver, args)
11
+ AppMap::Event::MethodCall.build_from_invocation(defined_class, hook_method, receiver, args)
12
+ end
12
13
 
13
- def handle_return(call_event_id, elapsed, return_value, exception)
14
- AppMap::Event::MethodReturn.build_from_invocation(call_event_id, return_value, exception, elapsed: elapsed)
15
- end
14
+ def handle_return(call_event_id, elapsed, return_value, exception)
15
+ AppMap::Event::MethodReturn.build_from_invocation(call_event_id, return_value, exception, elapsed: elapsed)
16
16
  end
17
17
  end
18
18
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'appmap/event'
4
+ require 'appmap/hook/method'
4
5
  require 'appmap/util'
5
6
  require 'rack'
6
7
 
@@ -81,29 +82,29 @@ module AppMap
81
82
  end
82
83
  end
83
84
 
84
- class NetHTTP
85
- class << self
86
- def copy_headers(obj)
87
- {}.tap do |headers|
88
- obj.each_header do |key, value|
89
- key = key.split('-').map(&:capitalize).join('-')
90
- headers[key] = value
91
- end
85
+ # Handler class for HTTP requests.
86
+ # Emits HTTP request events instead of method calls.
87
+ class NetHTTP < Hook::Method
88
+ def self.copy_headers(obj)
89
+ {}.tap do |headers|
90
+ obj.each_header do |key, value|
91
+ key = key.split('-').map(&:capitalize).join('-')
92
+ headers[key] = value
92
93
  end
93
94
  end
95
+ end
94
96
 
95
- def handle_call(defined_class, hook_method, receiver, args)
96
- # request will call itself again in a start block if it's not already started.
97
- return unless receiver.started?
97
+ def handle_call(receiver, args)
98
+ # request will call itself again in a start block if it's not already started.
99
+ return unless receiver.started?
98
100
 
99
- http = receiver
100
- request = args.first
101
- HTTPClientRequest.new(http, request)
102
- end
101
+ http = receiver
102
+ request = args.first
103
+ HTTPClientRequest.new(http, request)
104
+ end
103
105
 
104
- def handle_return(call_event_id, elapsed, return_value, exception)
105
- HTTPClientResponse.new(return_value, call_event_id, elapsed)
106
- end
106
+ def handle_return(call_event_id, elapsed, return_value, exception)
107
+ HTTPClientResponse.new(return_value, call_event_id, elapsed)
107
108
  end
108
109
  end
109
110
  end
@@ -100,15 +100,14 @@ module AppMap
100
100
 
101
101
  protected
102
102
 
103
- def before_hook(receiver, defined_class, _) # args
103
+ def before_hook(receiver, *)
104
104
  call_event = HTTPServerRequest.new(receiver.request)
105
105
  # http_server_request events are i/o and do not require a package name.
106
106
  AppMap.tracing.record_event call_event, defined_class: defined_class, method: hook_method
107
- [ call_event, TIME_NOW.call ]
107
+ call_event
108
108
  end
109
109
 
110
- def after_hook(receiver, call_event, start_time, _, _) # return_value, exception
111
- elapsed = TIME_NOW.call - start_time
110
+ def after_hook(receiver, call_event, elapsed, *)
112
111
  return_event = HTTPServerResponse.new receiver.response, call_event.id, elapsed
113
112
  AppMap.tracing.record_event return_event
114
113
  end