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