appmap 0.18.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.
- checksums.yaml +7 -0
- data/.dockerignore +5 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +100 -0
- data/Dockerfile.appmap +5 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +133 -0
- data/Rakefile +117 -0
- data/appmap.gemspec +41 -0
- data/appmap.yml +8 -0
- data/examples/install.rb +76 -0
- data/examples/mock_webapp/Gemfile +1 -0
- data/examples/mock_webapp/appmap.yml +2 -0
- data/examples/mock_webapp/exe/mock_webapp_request +12 -0
- data/examples/mock_webapp/lib/mock_webapp/controller.rb +23 -0
- data/examples/mock_webapp/lib/mock_webapp/request.rb +12 -0
- data/examples/mock_webapp/lib/mock_webapp/user.rb +18 -0
- data/exe/_appmap-record-self +49 -0
- data/exe/appmap +168 -0
- data/lib/appmap/algorithm/prune_class_map.rb +65 -0
- data/lib/appmap/command/inspect.rb +11 -0
- data/lib/appmap/command/record.rb +91 -0
- data/lib/appmap/command/upload.rb +103 -0
- data/lib/appmap/config/directory.rb +65 -0
- data/lib/appmap/config/file.rb +13 -0
- data/lib/appmap/config/named_function.rb +21 -0
- data/lib/appmap/config/package_dir.rb +52 -0
- data/lib/appmap/config/path.rb +25 -0
- data/lib/appmap/config.rb +65 -0
- data/lib/appmap/feature.rb +262 -0
- data/lib/appmap/inspect/inspector.rb +99 -0
- data/lib/appmap/inspect/parse_node.rb +170 -0
- data/lib/appmap/inspect/parser.rb +15 -0
- data/lib/appmap/inspect.rb +91 -0
- data/lib/appmap/middleware/remote_recording.rb +122 -0
- data/lib/appmap/parser.rb +60 -0
- data/lib/appmap/rails/action_handler.rb +77 -0
- data/lib/appmap/rails/sql_handler.rb +148 -0
- data/lib/appmap/railtie.rb +32 -0
- data/lib/appmap/rspec/parse_node.rb +41 -0
- data/lib/appmap/rspec/parser.rb +15 -0
- data/lib/appmap/rspec.rb +288 -0
- data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +65 -0
- data/lib/appmap/trace/tracer.rb +347 -0
- data/lib/appmap/version.rb +5 -0
- data/lib/appmap.rb +26 -0
- data/lore/pages/2019-05-21-install-and-record/index.pug +51 -0
- data/lore/pages/2019-05-21-install-and-record/install_example_appmap.png +0 -0
- data/lore/pages/2019-05-21-install-and-record/metadata.yml +5 -0
- data/lore/pages/layout.pug +66 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css +1912 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css +7 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css +331 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css +8 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css +9030 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css.map +1 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css +7 -0
- data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css.map +1 -0
- data/lore/public/stylesheets/style.css +8 -0
- data/package-lock.json +1066 -0
- data/package.json +24 -0
- data/spec/abstract_controller4_base_spec.rb +58 -0
- data/spec/abstract_controller_base_spec.rb +59 -0
- data/spec/fixtures/rack_users_app/.dockerignore +2 -0
- data/spec/fixtures/rack_users_app/.gitignore +2 -0
- data/spec/fixtures/rack_users_app/Dockerfile +32 -0
- data/spec/fixtures/rack_users_app/Gemfile +10 -0
- data/spec/fixtures/rack_users_app/appmap.yml +3 -0
- data/spec/fixtures/rack_users_app/config.ru +2 -0
- data/spec/fixtures/rack_users_app/docker-compose.yml +9 -0
- data/spec/fixtures/rack_users_app/lib/app.rb +36 -0
- data/spec/fixtures/rails4_users_app/.gitignore +13 -0
- data/spec/fixtures/rails4_users_app/.rbenv-gemsets +2 -0
- data/spec/fixtures/rails4_users_app/.ruby-version +1 -0
- data/spec/fixtures/rails4_users_app/Dockerfile +30 -0
- data/spec/fixtures/rails4_users_app/Dockerfile.pg +3 -0
- data/spec/fixtures/rails4_users_app/Gemfile +77 -0
- data/spec/fixtures/rails4_users_app/README.rdoc +28 -0
- data/spec/fixtures/rails4_users_app/Rakefile +6 -0
- data/spec/fixtures/rails4_users_app/app/assets/images/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/assets/javascripts/application.js +16 -0
- data/spec/fixtures/rails4_users_app/app/assets/stylesheets/application.css +15 -0
- data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +27 -0
- data/spec/fixtures/rails4_users_app/app/controllers/application_controller.rb +5 -0
- data/spec/fixtures/rails4_users_app/app/controllers/concerns/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/controllers/health_controller.rb +5 -0
- data/spec/fixtures/rails4_users_app/app/controllers/users_controller.rb +5 -0
- data/spec/fixtures/rails4_users_app/app/helpers/application_helper.rb +2 -0
- data/spec/fixtures/rails4_users_app/app/mailers/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/concerns/.keep +0 -0
- data/spec/fixtures/rails4_users_app/app/models/user.rb +18 -0
- data/spec/fixtures/rails4_users_app/app/views/layouts/application.html.haml +7 -0
- data/spec/fixtures/rails4_users_app/app/views/users/index.html.haml +7 -0
- data/spec/fixtures/rails4_users_app/appmap.yml +3 -0
- data/spec/fixtures/rails4_users_app/bin/rails +9 -0
- data/spec/fixtures/rails4_users_app/bin/setup +29 -0
- data/spec/fixtures/rails4_users_app/bin/spring +17 -0
- data/spec/fixtures/rails4_users_app/config/application.rb +26 -0
- data/spec/fixtures/rails4_users_app/config/boot.rb +3 -0
- data/spec/fixtures/rails4_users_app/config/database.yml +17 -0
- data/spec/fixtures/rails4_users_app/config/environment.rb +5 -0
- data/spec/fixtures/rails4_users_app/config/environments/development.rb +41 -0
- data/spec/fixtures/rails4_users_app/config/environments/production.rb +79 -0
- data/spec/fixtures/rails4_users_app/config/environments/test.rb +42 -0
- data/spec/fixtures/rails4_users_app/config/initializers/assets.rb +11 -0
- data/spec/fixtures/rails4_users_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/fixtures/rails4_users_app/config/initializers/cookies_serializer.rb +3 -0
- data/spec/fixtures/rails4_users_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/fixtures/rails4_users_app/config/initializers/inflections.rb +16 -0
- data/spec/fixtures/rails4_users_app/config/initializers/mime_types.rb +4 -0
- data/spec/fixtures/rails4_users_app/config/initializers/session_store.rb +3 -0
- data/spec/fixtures/rails4_users_app/config/initializers/to_time_preserves_timezone.rb +10 -0
- data/spec/fixtures/rails4_users_app/config/initializers/wrap_parameters.rb +14 -0
- data/spec/fixtures/rails4_users_app/config/locales/en.yml +23 -0
- data/spec/fixtures/rails4_users_app/config/routes.rb +12 -0
- data/spec/fixtures/rails4_users_app/config/secrets.yml +22 -0
- data/spec/fixtures/rails4_users_app/config.ru +4 -0
- data/spec/fixtures/rails4_users_app/create_app +23 -0
- data/spec/fixtures/rails4_users_app/db/migrate/20191127112304_create_users.rb +10 -0
- data/spec/fixtures/rails4_users_app/db/schema.rb +26 -0
- data/spec/fixtures/rails4_users_app/db/seeds.rb +7 -0
- data/spec/fixtures/rails4_users_app/docker-compose.yml +24 -0
- data/spec/fixtures/rails4_users_app/lib/assets/.keep +0 -0
- data/spec/fixtures/rails4_users_app/lib/tasks/.keep +0 -0
- data/spec/fixtures/rails4_users_app/log/.keep +0 -0
- data/spec/fixtures/rails4_users_app/public/404.html +67 -0
- data/spec/fixtures/rails4_users_app/public/422.html +67 -0
- data/spec/fixtures/rails4_users_app/public/500.html +66 -0
- data/spec/fixtures/rails4_users_app/public/favicon.ico +0 -0
- data/spec/fixtures/rails4_users_app/public/robots.txt +5 -0
- data/spec/fixtures/rails4_users_app/spec/controllers/users_controller_api_spec.rb +49 -0
- data/spec/fixtures/rails4_users_app/spec/rails_helper.rb +95 -0
- data/spec/fixtures/rails4_users_app/spec/spec_helper.rb +96 -0
- data/spec/fixtures/rails4_users_app/test/fixtures/users.yml +9 -0
- data/spec/fixtures/rails_users_app/.dockerignore +1 -0
- data/spec/fixtures/rails_users_app/.gitignore +39 -0
- data/spec/fixtures/rails_users_app/.rspec +1 -0
- data/spec/fixtures/rails_users_app/.ruby-version +1 -0
- data/spec/fixtures/rails_users_app/Dockerfile +29 -0
- data/spec/fixtures/rails_users_app/Dockerfile.pg +3 -0
- data/spec/fixtures/rails_users_app/Gemfile +51 -0
- data/spec/fixtures/rails_users_app/Rakefile +6 -0
- data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +27 -0
- data/spec/fixtures/rails_users_app/app/controllers/application_controller.rb +2 -0
- data/spec/fixtures/rails_users_app/app/controllers/concerns/.keep +0 -0
- data/spec/fixtures/rails_users_app/app/controllers/health_controller.rb +5 -0
- data/spec/fixtures/rails_users_app/app/controllers/users_controller.rb +5 -0
- data/spec/fixtures/rails_users_app/app/models/activerecord/user.rb +18 -0
- data/spec/fixtures/rails_users_app/app/models/concerns/.keep +0 -0
- data/spec/fixtures/rails_users_app/app/models/sequel/user.rb +25 -0
- data/spec/fixtures/rails_users_app/app/views/layouts/application.html.haml +7 -0
- data/spec/fixtures/rails_users_app/app/views/users/index.html.haml +7 -0
- data/spec/fixtures/rails_users_app/appmap.yml +3 -0
- data/spec/fixtures/rails_users_app/bin/_appmap-record-self +29 -0
- data/spec/fixtures/rails_users_app/bin/appmap +29 -0
- data/spec/fixtures/rails_users_app/bin/byebug +29 -0
- data/spec/fixtures/rails_users_app/bin/gli +29 -0
- data/spec/fixtures/rails_users_app/bin/htmldiff +29 -0
- data/spec/fixtures/rails_users_app/bin/ldiff +29 -0
- data/spec/fixtures/rails_users_app/bin/nokogiri +29 -0
- data/spec/fixtures/rails_users_app/bin/rackup +29 -0
- data/spec/fixtures/rails_users_app/bin/rails +4 -0
- data/spec/fixtures/rails_users_app/bin/rake +29 -0
- data/spec/fixtures/rails_users_app/bin/rspec +29 -0
- data/spec/fixtures/rails_users_app/bin/ruby-parse +29 -0
- data/spec/fixtures/rails_users_app/bin/ruby-rewrite +29 -0
- data/spec/fixtures/rails_users_app/bin/sequel +29 -0
- data/spec/fixtures/rails_users_app/bin/setup +25 -0
- data/spec/fixtures/rails_users_app/bin/sprockets +29 -0
- data/spec/fixtures/rails_users_app/bin/thor +29 -0
- data/spec/fixtures/rails_users_app/bin/update +25 -0
- data/spec/fixtures/rails_users_app/config/application.rb +51 -0
- data/spec/fixtures/rails_users_app/config/boot.rb +3 -0
- data/spec/fixtures/rails_users_app/config/credentials.yml.enc +1 -0
- data/spec/fixtures/rails_users_app/config/database.yml +17 -0
- data/spec/fixtures/rails_users_app/config/environment.rb +5 -0
- data/spec/fixtures/rails_users_app/config/environments/development.rb +40 -0
- data/spec/fixtures/rails_users_app/config/environments/production.rb +68 -0
- data/spec/fixtures/rails_users_app/config/environments/test.rb +36 -0
- data/spec/fixtures/rails_users_app/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/fixtures/rails_users_app/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/fixtures/rails_users_app/config/initializers/cors.rb +16 -0
- data/spec/fixtures/rails_users_app/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/fixtures/rails_users_app/config/initializers/inflections.rb +16 -0
- data/spec/fixtures/rails_users_app/config/initializers/mime_types.rb +4 -0
- data/spec/fixtures/rails_users_app/config/initializers/record_button.rb +3 -0
- data/spec/fixtures/rails_users_app/config/initializers/wrap_parameters.rb +9 -0
- data/spec/fixtures/rails_users_app/config/locales/en.yml +33 -0
- data/spec/fixtures/rails_users_app/config/routes.rb +11 -0
- data/spec/fixtures/rails_users_app/config.ru +5 -0
- data/spec/fixtures/rails_users_app/create_app +11 -0
- data/spec/fixtures/rails_users_app/db/migrate/20190728211408_create_users.rb +9 -0
- data/spec/fixtures/rails_users_app/db/schema.rb +23 -0
- data/spec/fixtures/rails_users_app/docker-compose.yml +24 -0
- data/spec/fixtures/rails_users_app/lib/tasks/.keep +0 -0
- data/spec/fixtures/rails_users_app/log/.keep +0 -0
- data/spec/fixtures/rails_users_app/public/robots.txt +1 -0
- data/spec/fixtures/rails_users_app/spec/controllers/users_controller_api_spec.rb +29 -0
- data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +39 -0
- data/spec/fixtures/rails_users_app/spec/rails_helper.rb +66 -0
- data/spec/fixtures/rails_users_app/spec/spec_helper.rb +96 -0
- data/spec/fixtures/rails_users_app/users_app/.gitignore +20 -0
- data/spec/rack_handler_webrick_spec.rb +59 -0
- data/spec/rails_spec_helper.rb +34 -0
- data/spec/railtie_spec.rb +35 -0
- data/spec/record_sql_rails4_pg_spec.rb +76 -0
- data/spec/record_sql_rails_pg_spec.rb +68 -0
- data/spec/rspec_feature_metadata_spec.rb +30 -0
- data/spec/spec_helper.rb +6 -0
- data/test/cli_test.rb +81 -0
- data/test/config_test.rb +149 -0
- data/test/explict_inspect_test.rb +29 -0
- data/test/fixtures/active_record_like/active_record/aggregations.rb +4 -0
- data/test/fixtures/active_record_like/active_record/association.rb +4 -0
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +8 -0
- data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +8 -0
- data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +6 -0
- data/test/fixtures/active_record_like/active_record/caps/caps.rb +4 -0
- data/test/fixtures/active_record_like/active_record.rb +2 -0
- data/test/fixtures/cli_record_test/appmap.yml +2 -0
- data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +7 -0
- data/test/fixtures/ignore_non_ruby_file/class.rb +3 -0
- data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +1 -0
- data/test/fixtures/includes_excludes/lib/a/a_1.rb +6 -0
- data/test/fixtures/includes_excludes/lib/a/a_2.rb +6 -0
- data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +8 -0
- data/test/fixtures/includes_excludes/lib/b/b_1.rb +6 -0
- data/test/fixtures/includes_excludes/lib/root_1.rb +4 -0
- data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +5 -0
- data/test/fixtures/inspect_multiple_subdirs/module_a.rb +2 -0
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +5 -0
- data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +5 -0
- data/test/fixtures/inspect_multiple_subdirs/module_b.rb +2 -0
- data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +6 -0
- data/test/fixtures/parse_file/defs_static_function.rb +96 -0
- data/test/fixtures/parse_file/function_within_class.rb +36 -0
- data/test/fixtures/parse_file/include_public_methods.rb +127 -0
- data/test/fixtures/parse_file/instance_function.rb +17 -0
- data/test/fixtures/parse_file/modules.rb +71 -0
- data/test/fixtures/parse_file/sclass_static_function.rb +88 -0
- data/test/fixtures/parse_file/toplevel_class.rb +13 -0
- data/test/fixtures/parse_file/toplevel_function.rb +14 -0
- data/test/fixtures/rspec_recorder/Gemfile +5 -0
- data/test/fixtures/rspec_recorder/appmap.yml +3 -0
- data/test/fixtures/rspec_recorder/lib/hello.rb +5 -0
- data/test/fixtures/rspec_recorder/spec/hello_spec.rb +9 -0
- data/test/fixtures/trace_test/trace_program_1.rb +44 -0
- data/test/implicit_inspect_test.rb +33 -0
- data/test/include_exclude_test.rb +48 -0
- data/test/prerecorded_trace_test.rb +76 -0
- data/test/rspec_test.rb +22 -0
- data/test/test_helper.rb +46 -0
- data/test/trace_test.rb +92 -0
- metadata +501 -0
data/lib/appmap/rspec.rb
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'appmap'
|
|
4
|
+
require 'appmap/config'
|
|
5
|
+
require 'appmap/inspect'
|
|
6
|
+
require 'appmap/trace/tracer'
|
|
7
|
+
|
|
8
|
+
require 'active_support/inflector/transliterate'
|
|
9
|
+
|
|
10
|
+
module AppMap
|
|
11
|
+
# Integration of AppMap with RSpec. When enabled with APPMAP=true, the AppMap tracer will
|
|
12
|
+
# be activated around each scenario which has the metadata key `:appmap`.
|
|
13
|
+
module RSpec
|
|
14
|
+
APPMAP_OUTPUT_DIR = 'tmp/appmap/rspec'
|
|
15
|
+
|
|
16
|
+
class Recorder
|
|
17
|
+
attr_reader :config, :features, :functions
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@config = AppMap::Config.load_from_file('appmap.yml')
|
|
21
|
+
|
|
22
|
+
raise "Missing AppMap configuration setting: 'name'" unless @config.name
|
|
23
|
+
|
|
24
|
+
@features = AppMap.inspect(@config)
|
|
25
|
+
@functions = @features.map(&:collect_functions).flatten
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def setup
|
|
29
|
+
FileUtils.mkdir_p APPMAP_OUTPUT_DIR
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# TODO: Optionally populate the 'layout' from appmap config or RSpec metadata.
|
|
33
|
+
def save(example_name, events, feature_name: nil, feature_group_name: nil)
|
|
34
|
+
require 'appmap/command/record'
|
|
35
|
+
metadata = AppMap::Command::Record.detect_metadata.tap do |m|
|
|
36
|
+
m[:name] = example_name
|
|
37
|
+
m[:app] = @config.name
|
|
38
|
+
m[:feature] = feature_name if feature_name
|
|
39
|
+
m[:feature_group] = feature_group_name if feature_group_name
|
|
40
|
+
m[:frameworks] ||= []
|
|
41
|
+
m[:frameworks] << {
|
|
42
|
+
name: 'rspec',
|
|
43
|
+
version: Gem.loaded_specs['rspec-core']&.version&.to_s
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
appmap = {
|
|
48
|
+
version: '1.1',
|
|
49
|
+
classMap: features,
|
|
50
|
+
metadata: metadata,
|
|
51
|
+
events: events
|
|
52
|
+
}
|
|
53
|
+
fname = sanitize_filename(example_name)
|
|
54
|
+
File.write(File.join(APPMAP_OUTPUT_DIR, "#{fname}.appmap.json"), JSON.generate(appmap))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Cribbed from v5 version of ActiveSupport:Inflector#parameterize:
|
|
58
|
+
# https://github.com/rails/rails/blob/v5.2.4/activesupport/lib/active_support/inflector/transliterate.rb#L92
|
|
59
|
+
def sanitize_filename(fname, separator: '_')
|
|
60
|
+
# Replace accented chars with their ASCII equivalents.
|
|
61
|
+
fname = ActiveSupport::Inflector.transliterate(fname)
|
|
62
|
+
|
|
63
|
+
# Turn unwanted chars into the separator.
|
|
64
|
+
fname.gsub!(/[^a-z0-9\-_]+/i, separator)
|
|
65
|
+
|
|
66
|
+
re_sep = Regexp.escape(separator)
|
|
67
|
+
re_duplicate_separator = /#{re_sep}{2,}/
|
|
68
|
+
re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
|
|
69
|
+
|
|
70
|
+
# No more than one of the separator in a row.
|
|
71
|
+
fname.gsub!(re_duplicate_separator, separator)
|
|
72
|
+
|
|
73
|
+
# Finally, Remove leading/trailing separator.
|
|
74
|
+
fname.gsub(re_leading_trailing_separator, '')
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class << self
|
|
79
|
+
module FeatureAnnotations
|
|
80
|
+
def feature
|
|
81
|
+
return nil unless annotations
|
|
82
|
+
|
|
83
|
+
annotations[:feature]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def feature_group
|
|
87
|
+
return nil unless annotations
|
|
88
|
+
|
|
89
|
+
annotations[:feature_group]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def annotations
|
|
93
|
+
metadata.tap do |md|
|
|
94
|
+
description_args_hashes.each do |h|
|
|
95
|
+
md.merge! h
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
protected
|
|
101
|
+
|
|
102
|
+
def metadata
|
|
103
|
+
return {} unless example_obj.respond_to?(:metadata)
|
|
104
|
+
|
|
105
|
+
example_obj.metadata
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def description_args_hashes
|
|
109
|
+
return [] unless example_obj.respond_to?(:metadata)
|
|
110
|
+
|
|
111
|
+
(example_obj.metadata[:description_args] || []).select { |arg| arg.is_a?(Hash) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# ScopeExample and ScopeExampleGroup is a way to handle the weird way that RSpec
|
|
116
|
+
# stores the nested example names.
|
|
117
|
+
ScopeExample = Struct.new(:example) do
|
|
118
|
+
include FeatureAnnotations
|
|
119
|
+
|
|
120
|
+
alias_method :example_obj, :example
|
|
121
|
+
|
|
122
|
+
def description
|
|
123
|
+
example.description
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def parent
|
|
127
|
+
ScopeExampleGroup.new(example.example_group)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# As you can see here, the way that RSpec stores the example description and
|
|
132
|
+
# represents the example group hierarchy is pretty weird.
|
|
133
|
+
ScopeExampleGroup = Struct.new(:example_group) do
|
|
134
|
+
include FeatureAnnotations
|
|
135
|
+
|
|
136
|
+
alias_method :example_obj, :example_group
|
|
137
|
+
|
|
138
|
+
def description_args
|
|
139
|
+
# Don't stringify any hashes that RSpec considers part of the example group description.
|
|
140
|
+
example_group.metadata[:description_args].reject { |arg| arg.is_a?(Hash) }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def description?
|
|
144
|
+
return true if example_group.respond_to?(:described_class) && example_group.described_class
|
|
145
|
+
|
|
146
|
+
return true if example_group.respond_to?(:description) && !description_args.empty?
|
|
147
|
+
|
|
148
|
+
false
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def description
|
|
152
|
+
description? ? description_args.join(' ') : nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def parent
|
|
156
|
+
# An example group always has a parent; but it might be 'self'...
|
|
157
|
+
example_group.parent != example_group ? ScopeExampleGroup.new(example_group.parent) : nil
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
LOG = false
|
|
162
|
+
|
|
163
|
+
def is_example_group_subclass_call?(tp)
|
|
164
|
+
# Order is important here. Checking for method_id == :subclass
|
|
165
|
+
# first will avoid calling defined_class.to_s in many cases,
|
|
166
|
+
# some of which will fail.
|
|
167
|
+
#
|
|
168
|
+
# For example, ActiveRecord in Rails 4 defines #inspect (and
|
|
169
|
+
# therefore #to_s) in such a way that it will fail if called
|
|
170
|
+
# here.
|
|
171
|
+
tp.event == :call &&
|
|
172
|
+
tp.method_id == :subclass &&
|
|
173
|
+
tp.defined_class.singleton_class? &&
|
|
174
|
+
tp.defined_class.to_s == '#<Class:RSpec::Core::ExampleGroup>'
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def is_example_initialize_call?(tp)
|
|
178
|
+
tp.event == :call &&
|
|
179
|
+
tp.method_id == :initialize &&
|
|
180
|
+
tp.defined_class.to_s == 'RSpec::Core::Example'
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def generate_appmaps_from_specs
|
|
184
|
+
recorder = Recorder.new
|
|
185
|
+
recorder.setup
|
|
186
|
+
|
|
187
|
+
require 'set'
|
|
188
|
+
# file:lineno at which an Example block begins
|
|
189
|
+
trace_block_start = Set.new
|
|
190
|
+
# file:lineno at which an Example block ends
|
|
191
|
+
trace_block_end = Set.new
|
|
192
|
+
|
|
193
|
+
# value: a BlockParseNode from an RSpec file
|
|
194
|
+
# key: file:lineno at which the block begins
|
|
195
|
+
rspec_blocks = {}
|
|
196
|
+
|
|
197
|
+
# value: an Example instance
|
|
198
|
+
# key: file:lineno at which the Example block ends
|
|
199
|
+
examples = {}
|
|
200
|
+
|
|
201
|
+
current_tracer = nil
|
|
202
|
+
|
|
203
|
+
TracePoint.trace(:call, :b_call, :b_return) do |tp|
|
|
204
|
+
# When a new ExampleGroup is encountered, parse the source file containing it and look
|
|
205
|
+
# for blocks that might be Examples. Index each BlockParseNode by the start file:lineno.
|
|
206
|
+
if is_example_group_subclass_call?(tp)
|
|
207
|
+
example_block = tp.binding.eval('example_group_block')
|
|
208
|
+
source_path, start_line = example_block.source_location
|
|
209
|
+
require 'appmap/rspec/parser'
|
|
210
|
+
nodes, = AppMap::RSpec::Parser.new(file_path: source_path).parse
|
|
211
|
+
nodes.each do |node|
|
|
212
|
+
start_loc = [ node.file_path, node.first_line ].join(':')
|
|
213
|
+
rspec_blocks[start_loc] = node
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# When a new Example is constructed with a block, look for the BlockParseNode that starts at the block's
|
|
218
|
+
# file:lineno. If it exists, store the Example object, indexed by the file:lineno at which it ends.
|
|
219
|
+
if is_example_initialize_call?(tp)
|
|
220
|
+
example_block = tp.binding.eval('example_block')
|
|
221
|
+
if example_block
|
|
222
|
+
source_path, start_line = example_block.source_location
|
|
223
|
+
start_loc = [ source_path, start_line ].join(':')
|
|
224
|
+
if (rspec_block = rspec_blocks[start_loc])
|
|
225
|
+
end_loc = [ source_path, rspec_block.last_line ].join(':')
|
|
226
|
+
trace_block_start << start_loc.tap { |loc| puts "Start: #{loc}" if LOG }
|
|
227
|
+
trace_block_end << end_loc.tap { |loc| puts "End: #{loc}" if LOG }
|
|
228
|
+
examples[end_loc] = tp.binding.eval('self')
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
if %i[b_call b_return].member?(tp.event)
|
|
234
|
+
loc = [ tp.path, tp.lineno ].join(':')
|
|
235
|
+
puts loc if LOG && (trace_block_start.member?(loc) || trace_block_end.member?(loc))
|
|
236
|
+
|
|
237
|
+
# When a new block is started, check if an Example block is known to begin at that
|
|
238
|
+
# file:lineno. If it is, enable the AppMap tracer.
|
|
239
|
+
if tp.event == :b_call && trace_block_start.member?(loc)
|
|
240
|
+
puts "Starting trace on #{loc}" if LOG
|
|
241
|
+
current_tracer = AppMap::Trace.tracers.trace(recorder.functions)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# When the tracer is enabled and a block is completed, check to see if there is an
|
|
245
|
+
# Example stored at the file:lineno. If so, finish tracing and emit the
|
|
246
|
+
# AppMap file.
|
|
247
|
+
if current_tracer && tp.event == :b_return && trace_block_end.member?(loc)
|
|
248
|
+
puts "Ending trace on #{loc}" if LOG
|
|
249
|
+
events = []
|
|
250
|
+
AppMap::Trace.tracers.delete current_tracer
|
|
251
|
+
|
|
252
|
+
while current_tracer.event?
|
|
253
|
+
events << current_tracer.next_event.to_h
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
example = examples[loc]
|
|
257
|
+
description = []
|
|
258
|
+
scope = ScopeExample.new(example)
|
|
259
|
+
feature_group = feature = nil
|
|
260
|
+
while scope
|
|
261
|
+
description << scope.description
|
|
262
|
+
feature ||= scope.feature
|
|
263
|
+
feature_group ||= scope.feature_group
|
|
264
|
+
scope = scope.parent
|
|
265
|
+
end
|
|
266
|
+
description.reject! { |d| d.nil? || d == '' }
|
|
267
|
+
description.reverse!
|
|
268
|
+
|
|
269
|
+
description.each do |token|
|
|
270
|
+
token = token.gsub 'it should behave like', ''
|
|
271
|
+
token.gsub! ' ', ' '
|
|
272
|
+
token.gsub! '/', '_'
|
|
273
|
+
token.strip!
|
|
274
|
+
end
|
|
275
|
+
full_description = description.join(' ')
|
|
276
|
+
|
|
277
|
+
recorder.save full_description, events,
|
|
278
|
+
feature_name: feature,
|
|
279
|
+
feature_group_name: feature_group
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
generate_appmaps_from_specs if ENV['APPMAP'] == 'true'
|
|
287
|
+
end
|
|
288
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
module AppMap
|
|
2
|
+
module Trace
|
|
3
|
+
module EventHandler
|
|
4
|
+
# Use the `req` and `res` parameters on Rack::Handler::WEBrick to populate the
|
|
5
|
+
# `http_server_request` and `http_server_response` info on the trace event.
|
|
6
|
+
#
|
|
7
|
+
# See https://github.com/rack/rack/blob/b72bfc9435c118c54019efae1fedd119521b76df/lib/rack/handler/webrick.rb#L26
|
|
8
|
+
module RackHandlerWebrick
|
|
9
|
+
class Call < MethodEvent
|
|
10
|
+
attr_accessor :http_server_request
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def build_from_tracepoint(mc = Call.new, tp, path)
|
|
14
|
+
mc.tap do |_|
|
|
15
|
+
req = value_in_binding(tp, :req)
|
|
16
|
+
|
|
17
|
+
# Don't try and grab 'parameters', because:
|
|
18
|
+
# a) They aren't needed.
|
|
19
|
+
# b) We want to avoid triggering side effects like reading the request body.
|
|
20
|
+
|
|
21
|
+
mc.http_server_request = {
|
|
22
|
+
request_method: req.request_method,
|
|
23
|
+
path_info: req.path_info,
|
|
24
|
+
protocol: "HTTP/#{req.http_version}"
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
MethodEvent.build_from_tracepoint(mc, tp, path)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_h
|
|
33
|
+
super.tap do |h|
|
|
34
|
+
h[:http_server_request] = http_server_request
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class Return < MethodReturnIgnoreValue
|
|
40
|
+
attr_accessor :http_server_response
|
|
41
|
+
|
|
42
|
+
class << self
|
|
43
|
+
def build_from_tracepoint(mr = Return.new, tp, path, parent_id, elapsed)
|
|
44
|
+
mr.tap do |_|
|
|
45
|
+
res = value_in_binding(tp, :res)
|
|
46
|
+
|
|
47
|
+
mr.http_server_response = {
|
|
48
|
+
status: res.status
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
MethodReturnIgnoreValue.build_from_tracepoint(mr, tp, path, parent_id, elapsed)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
super.tap do |h|
|
|
58
|
+
h[:http_server_response] = http_server_response
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
module AppMap
|
|
2
|
+
module Trace
|
|
3
|
+
MethodEventStruct =
|
|
4
|
+
Struct.new(:id, :event, :defined_class, :method_id, :path, :lineno, :static, :thread_id)
|
|
5
|
+
|
|
6
|
+
class Tracers
|
|
7
|
+
def initialize
|
|
8
|
+
@tracers = []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def empty?
|
|
12
|
+
@tracers.empty?
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def trace(functions, enable: true)
|
|
16
|
+
AppMap::Trace::Tracer.new(functions).tap do |tracer|
|
|
17
|
+
@tracers << tracer
|
|
18
|
+
tracer.enable if enable
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def record_event(event)
|
|
23
|
+
@tracers.each do |tracer|
|
|
24
|
+
tracer.record_event(event)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete(tracer)
|
|
29
|
+
return unless @tracers.member?(tracer)
|
|
30
|
+
|
|
31
|
+
@tracers.delete(tracer)
|
|
32
|
+
tracer.disable
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def tracers
|
|
38
|
+
@tracers ||= Tracers.new
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @appmap
|
|
43
|
+
class MethodEvent < MethodEventStruct
|
|
44
|
+
LIMIT = 100
|
|
45
|
+
|
|
46
|
+
COUNTER_LOCK = Mutex.new # :nodoc:
|
|
47
|
+
@@id_counter = 0
|
|
48
|
+
|
|
49
|
+
class << self
|
|
50
|
+
# Build a new instance from a TracePoint.
|
|
51
|
+
def build_from_tracepoint(me, tp, path)
|
|
52
|
+
me.id = next_id
|
|
53
|
+
me.event = tp.event
|
|
54
|
+
|
|
55
|
+
if tp.defined_class.singleton_class?
|
|
56
|
+
me.defined_class = (tp.self.is_a?(Class) || tp.self.is_a?(Module)) ? tp.self.name : tp.self.class.name
|
|
57
|
+
else
|
|
58
|
+
me.defined_class = tp.defined_class.name
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
me.method_id = tp.method_id
|
|
62
|
+
me.path = path
|
|
63
|
+
me.lineno = tp.lineno
|
|
64
|
+
me.static = tp.defined_class.name.nil?
|
|
65
|
+
me.thread_id = Thread.current.object_id
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Gets the next serial id.
|
|
69
|
+
#
|
|
70
|
+
# This method is thread-safe.
|
|
71
|
+
# @appmap
|
|
72
|
+
def next_id
|
|
73
|
+
COUNTER_LOCK.synchronize do
|
|
74
|
+
@@id_counter += 1
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Gets a value, by key, from the trace point binding.
|
|
79
|
+
# If the method raises an error, it can be handled by the optional block.
|
|
80
|
+
def value_in_binding(tp, key, &block)
|
|
81
|
+
tp.binding.eval(key.to_s)
|
|
82
|
+
rescue NameError, ArgumentError
|
|
83
|
+
yield if block_given?
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Gets a display string for a value. This is not meant to be a machine deserializable value.
|
|
87
|
+
def display_string(value)
|
|
88
|
+
return nil unless value
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
value_string = value.to_s
|
|
92
|
+
rescue NoMethodError
|
|
93
|
+
value_string = value.inspect
|
|
94
|
+
rescue StandardError
|
|
95
|
+
warn $!.message
|
|
96
|
+
'*Error inspecting variable*'
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
value_string[0...LIMIT].encode('utf-8', invalid: :replace, undef: :replace, replace: '_')
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
alias static? static
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# @appmap
|
|
107
|
+
class MethodCall < MethodEvent
|
|
108
|
+
attr_accessor :parameters, :receiver
|
|
109
|
+
|
|
110
|
+
class << self
|
|
111
|
+
# @appmap
|
|
112
|
+
def build_from_tracepoint(mc = MethodCall.new, tp, path)
|
|
113
|
+
mc.tap do |_|
|
|
114
|
+
mc.parameters = collect_parameters(tp)
|
|
115
|
+
mc.receiver = collect_self(tp)
|
|
116
|
+
MethodEvent.build_from_tracepoint(mc, tp, path)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def collect_self(tp)
|
|
121
|
+
{
|
|
122
|
+
class: tp.self.class.name,
|
|
123
|
+
object_id: tp.self.__id__,
|
|
124
|
+
value: display_string(tp.self)
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def collect_parameters(tp)
|
|
129
|
+
tp.self.method(tp.method_id).parameters.collect do |pinfo|
|
|
130
|
+
kind, key = pinfo
|
|
131
|
+
value = value_in_binding(tp, key)
|
|
132
|
+
{
|
|
133
|
+
name: key,
|
|
134
|
+
class: value.class.name,
|
|
135
|
+
object_id: value.__id__,
|
|
136
|
+
value: display_string(value),
|
|
137
|
+
kind: kind # :req, :rest, :key, :keyrest, :block
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def to_h
|
|
144
|
+
super.tap do |h|
|
|
145
|
+
h[:parameters] = parameters
|
|
146
|
+
h[:receiver] = receiver
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class MethodReturnIgnoreValue < MethodEvent
|
|
152
|
+
attr_accessor :parent_id, :elapsed
|
|
153
|
+
|
|
154
|
+
class << self
|
|
155
|
+
def build_from_tracepoint(mr = MethodReturnIgnoreValue.new, tp, path, parent_id, elapsed)
|
|
156
|
+
mr.tap do |_|
|
|
157
|
+
mr.parent_id = parent_id
|
|
158
|
+
mr.elapsed = elapsed
|
|
159
|
+
MethodEvent.build_from_tracepoint(mr, tp, path)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def to_h
|
|
165
|
+
super.tap do |h|
|
|
166
|
+
h[:parent_id] = parent_id
|
|
167
|
+
h[:elapsed] = elapsed
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
class MethodReturn < MethodReturnIgnoreValue
|
|
173
|
+
attr_accessor :return_value
|
|
174
|
+
|
|
175
|
+
class << self
|
|
176
|
+
def build_from_tracepoint(mr = MethodReturn.new, tp, path, parent_id, elapsed)
|
|
177
|
+
mr.tap do |_|
|
|
178
|
+
mr.return_value = {
|
|
179
|
+
class: tp.return_value.class.name,
|
|
180
|
+
value: display_string(tp.return_value),
|
|
181
|
+
object_id: tp.return_value.__id__
|
|
182
|
+
}
|
|
183
|
+
MethodReturnIgnoreValue.build_from_tracepoint(mr, tp, path, parent_id, elapsed)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def to_h
|
|
189
|
+
super.tap do |h|
|
|
190
|
+
h[:return_value] = return_value
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Processes a series of calls into recorded events.
|
|
196
|
+
# Each call to the handle should provide a TracePoint (or duck-typed object) as the argument.
|
|
197
|
+
# On each call, a MethodEvent is constructed according to the nature of the TracePoint, and then
|
|
198
|
+
# stored using the record_event method.
|
|
199
|
+
# @appmap
|
|
200
|
+
class TracePointHandler
|
|
201
|
+
attr_accessor :call_constructor, :return_constructor
|
|
202
|
+
|
|
203
|
+
DEFAULT_HANDLER_CLASSES = {
|
|
204
|
+
call: MethodCall,
|
|
205
|
+
return: MethodReturn
|
|
206
|
+
}.freeze
|
|
207
|
+
|
|
208
|
+
# @appmap
|
|
209
|
+
def initialize(tracer)
|
|
210
|
+
@pwd = Dir.pwd
|
|
211
|
+
@tracer = tracer
|
|
212
|
+
@call_stack = Hash.new { |h, k| h[k] = [] }
|
|
213
|
+
@handler_classes = {}
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# @appmap
|
|
217
|
+
def handle(tp)
|
|
218
|
+
# Absoute paths which are within the current working directory are normalized
|
|
219
|
+
# to be relative paths.
|
|
220
|
+
path = tp.path
|
|
221
|
+
if path.index(@pwd) == 0
|
|
222
|
+
path = path[@pwd.length+1..-1]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
method_event = \
|
|
226
|
+
if tp.event == :call && (function = @tracer.lookup_function(path, tp.lineno))
|
|
227
|
+
call_constructor = handler_class(function, tp.event)
|
|
228
|
+
call_constructor.build_from_tracepoint(tp, path).tap do |c|
|
|
229
|
+
@call_stack[Thread.current.object_id] << [ tp.defined_class, tp.method_id, c.id, Time.now, function ]
|
|
230
|
+
end
|
|
231
|
+
elsif (c = @call_stack[Thread.current.object_id].last) &&
|
|
232
|
+
c[0] == tp.defined_class &&
|
|
233
|
+
c[1] == tp.method_id
|
|
234
|
+
function = c[4]
|
|
235
|
+
@call_stack[Thread.current.object_id].pop
|
|
236
|
+
return_constructor = handler_class(function, tp.event)
|
|
237
|
+
return_constructor.build_from_tracepoint(tp, path, c[2], Time.now - c[3])
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
@tracer.record_event method_event if method_event
|
|
241
|
+
|
|
242
|
+
method_event
|
|
243
|
+
rescue
|
|
244
|
+
puts $!.message
|
|
245
|
+
puts $!.backtrace.join("\n")
|
|
246
|
+
# XXX If this exception doesn't get reraised, internal errors
|
|
247
|
+
# (e.g. a missing method on TracePoint) get silently
|
|
248
|
+
# ignored. This allows tests to pass that should fail, which
|
|
249
|
+
# is bad, but is it desirable otherwise?
|
|
250
|
+
raise
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
protected
|
|
254
|
+
|
|
255
|
+
# Figure out which handler class should be used for a trace event. It may be
|
|
256
|
+
# a custom handler, e.g. in case we are processing a special named function such as a
|
|
257
|
+
# web server entry point, or it may be the standard :call or :return handler.
|
|
258
|
+
def handler_class(function, event)
|
|
259
|
+
cache_key = [function.location, event]
|
|
260
|
+
cached_handler = @handler_classes[cache_key]
|
|
261
|
+
return cached_handler if cached_handler
|
|
262
|
+
|
|
263
|
+
return default_handler_class(event) unless function.handler_id
|
|
264
|
+
|
|
265
|
+
require "appmap/trace/event_handler/#{function.handler_id}"
|
|
266
|
+
|
|
267
|
+
AppMap::Trace::EventHandler
|
|
268
|
+
.const_get(function.handler_id.to_s.camelize)
|
|
269
|
+
.const_get(event.to_s.capitalize).tap do |handler|
|
|
270
|
+
@handler_classes[cache_key] = handler
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def default_handler_class(event)
|
|
275
|
+
DEFAULT_HANDLER_CLASSES[event] or raise "No handler class for #{event.inspect}"
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
# @appmap
|
|
280
|
+
class Tracer
|
|
281
|
+
# Trace a specified set of functions.
|
|
282
|
+
#
|
|
283
|
+
# functions Array of AppMap::Feature::Function.
|
|
284
|
+
# @appmap
|
|
285
|
+
def initialize(functions)
|
|
286
|
+
@functions = functions
|
|
287
|
+
|
|
288
|
+
@functions_by_location = functions.each_with_object({}) do |m, memo|
|
|
289
|
+
path, lineno = m.location.split(':', 2)
|
|
290
|
+
memo[path] ||= {}
|
|
291
|
+
memo[path][lineno.to_i] = m
|
|
292
|
+
memo
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
@events_mutex = Mutex.new
|
|
296
|
+
@events = []
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def enable
|
|
300
|
+
handler = TracePointHandler.new(self)
|
|
301
|
+
@trace_point = TracePoint.trace(:call, :return, &handler.method(:handle))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Private function. Use AppMap.tracers#delete.
|
|
305
|
+
def disable # :nodoc:
|
|
306
|
+
@trace_point.disable
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Whether the indicated file path and lineno is a breakpoint on which
|
|
310
|
+
# execution should interrupted.
|
|
311
|
+
# @appmap
|
|
312
|
+
def lookup_function(path, lineno)
|
|
313
|
+
(methods_by_path = @functions_by_location[path]) && methods_by_path[lineno]
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Record a program execution event.
|
|
317
|
+
#
|
|
318
|
+
# The event should be one of the MethodEvent subclasses.
|
|
319
|
+
#
|
|
320
|
+
# This method is thread-safe.
|
|
321
|
+
# @appmap
|
|
322
|
+
def record_event(event)
|
|
323
|
+
@events_mutex.synchronize do
|
|
324
|
+
@events << event
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Whether there is an event available for processing.
|
|
329
|
+
#
|
|
330
|
+
# This method is thread-safe.
|
|
331
|
+
def event?
|
|
332
|
+
@events_mutex.synchronize do
|
|
333
|
+
!@events.empty?
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
# Gets the next available event, if any.
|
|
338
|
+
#
|
|
339
|
+
# This method is thread-safe.
|
|
340
|
+
def next_event
|
|
341
|
+
@events_mutex.synchronize do
|
|
342
|
+
@events.shift
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|