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.
Files changed (262) hide show
  1. checksums.yaml +7 -0
  2. data/.dockerignore +5 -0
  3. data/.gitignore +17 -0
  4. data/.rubocop.yml +18 -0
  5. data/.ruby-version +1 -0
  6. data/CHANGELOG.md +100 -0
  7. data/Dockerfile.appmap +5 -0
  8. data/Gemfile +5 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +133 -0
  11. data/Rakefile +117 -0
  12. data/appmap.gemspec +41 -0
  13. data/appmap.yml +8 -0
  14. data/examples/install.rb +76 -0
  15. data/examples/mock_webapp/Gemfile +1 -0
  16. data/examples/mock_webapp/appmap.yml +2 -0
  17. data/examples/mock_webapp/exe/mock_webapp_request +12 -0
  18. data/examples/mock_webapp/lib/mock_webapp/controller.rb +23 -0
  19. data/examples/mock_webapp/lib/mock_webapp/request.rb +12 -0
  20. data/examples/mock_webapp/lib/mock_webapp/user.rb +18 -0
  21. data/exe/_appmap-record-self +49 -0
  22. data/exe/appmap +168 -0
  23. data/lib/appmap/algorithm/prune_class_map.rb +65 -0
  24. data/lib/appmap/command/inspect.rb +11 -0
  25. data/lib/appmap/command/record.rb +91 -0
  26. data/lib/appmap/command/upload.rb +103 -0
  27. data/lib/appmap/config/directory.rb +65 -0
  28. data/lib/appmap/config/file.rb +13 -0
  29. data/lib/appmap/config/named_function.rb +21 -0
  30. data/lib/appmap/config/package_dir.rb +52 -0
  31. data/lib/appmap/config/path.rb +25 -0
  32. data/lib/appmap/config.rb +65 -0
  33. data/lib/appmap/feature.rb +262 -0
  34. data/lib/appmap/inspect/inspector.rb +99 -0
  35. data/lib/appmap/inspect/parse_node.rb +170 -0
  36. data/lib/appmap/inspect/parser.rb +15 -0
  37. data/lib/appmap/inspect.rb +91 -0
  38. data/lib/appmap/middleware/remote_recording.rb +122 -0
  39. data/lib/appmap/parser.rb +60 -0
  40. data/lib/appmap/rails/action_handler.rb +77 -0
  41. data/lib/appmap/rails/sql_handler.rb +148 -0
  42. data/lib/appmap/railtie.rb +32 -0
  43. data/lib/appmap/rspec/parse_node.rb +41 -0
  44. data/lib/appmap/rspec/parser.rb +15 -0
  45. data/lib/appmap/rspec.rb +288 -0
  46. data/lib/appmap/trace/event_handler/rack_handler_webrick.rb +65 -0
  47. data/lib/appmap/trace/tracer.rb +347 -0
  48. data/lib/appmap/version.rb +5 -0
  49. data/lib/appmap.rb +26 -0
  50. data/lore/pages/2019-05-21-install-and-record/index.pug +51 -0
  51. data/lore/pages/2019-05-21-install-and-record/install_example_appmap.png +0 -0
  52. data/lore/pages/2019-05-21-install-and-record/metadata.yml +5 -0
  53. data/lore/pages/layout.pug +66 -0
  54. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css +1912 -0
  55. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.css.map +1 -0
  56. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css +7 -0
  57. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-grid.min.css.map +1 -0
  58. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css +331 -0
  59. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.css.map +1 -0
  60. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css +8 -0
  61. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap-reboot.min.css.map +1 -0
  62. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css +9030 -0
  63. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.css.map +1 -0
  64. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css +7 -0
  65. data/lore/public/lib/bootstrap-4.1.3/css/bootstrap.min.css.map +1 -0
  66. data/lore/public/stylesheets/style.css +8 -0
  67. data/package-lock.json +1066 -0
  68. data/package.json +24 -0
  69. data/spec/abstract_controller4_base_spec.rb +58 -0
  70. data/spec/abstract_controller_base_spec.rb +59 -0
  71. data/spec/fixtures/rack_users_app/.dockerignore +2 -0
  72. data/spec/fixtures/rack_users_app/.gitignore +2 -0
  73. data/spec/fixtures/rack_users_app/Dockerfile +32 -0
  74. data/spec/fixtures/rack_users_app/Gemfile +10 -0
  75. data/spec/fixtures/rack_users_app/appmap.yml +3 -0
  76. data/spec/fixtures/rack_users_app/config.ru +2 -0
  77. data/spec/fixtures/rack_users_app/docker-compose.yml +9 -0
  78. data/spec/fixtures/rack_users_app/lib/app.rb +36 -0
  79. data/spec/fixtures/rails4_users_app/.gitignore +13 -0
  80. data/spec/fixtures/rails4_users_app/.rbenv-gemsets +2 -0
  81. data/spec/fixtures/rails4_users_app/.ruby-version +1 -0
  82. data/spec/fixtures/rails4_users_app/Dockerfile +30 -0
  83. data/spec/fixtures/rails4_users_app/Dockerfile.pg +3 -0
  84. data/spec/fixtures/rails4_users_app/Gemfile +77 -0
  85. data/spec/fixtures/rails4_users_app/README.rdoc +28 -0
  86. data/spec/fixtures/rails4_users_app/Rakefile +6 -0
  87. data/spec/fixtures/rails4_users_app/app/assets/images/.keep +0 -0
  88. data/spec/fixtures/rails4_users_app/app/assets/javascripts/application.js +16 -0
  89. data/spec/fixtures/rails4_users_app/app/assets/stylesheets/application.css +15 -0
  90. data/spec/fixtures/rails4_users_app/app/controllers/api/users_controller.rb +27 -0
  91. data/spec/fixtures/rails4_users_app/app/controllers/application_controller.rb +5 -0
  92. data/spec/fixtures/rails4_users_app/app/controllers/concerns/.keep +0 -0
  93. data/spec/fixtures/rails4_users_app/app/controllers/health_controller.rb +5 -0
  94. data/spec/fixtures/rails4_users_app/app/controllers/users_controller.rb +5 -0
  95. data/spec/fixtures/rails4_users_app/app/helpers/application_helper.rb +2 -0
  96. data/spec/fixtures/rails4_users_app/app/mailers/.keep +0 -0
  97. data/spec/fixtures/rails4_users_app/app/models/.keep +0 -0
  98. data/spec/fixtures/rails4_users_app/app/models/concerns/.keep +0 -0
  99. data/spec/fixtures/rails4_users_app/app/models/user.rb +18 -0
  100. data/spec/fixtures/rails4_users_app/app/views/layouts/application.html.haml +7 -0
  101. data/spec/fixtures/rails4_users_app/app/views/users/index.html.haml +7 -0
  102. data/spec/fixtures/rails4_users_app/appmap.yml +3 -0
  103. data/spec/fixtures/rails4_users_app/bin/rails +9 -0
  104. data/spec/fixtures/rails4_users_app/bin/setup +29 -0
  105. data/spec/fixtures/rails4_users_app/bin/spring +17 -0
  106. data/spec/fixtures/rails4_users_app/config/application.rb +26 -0
  107. data/spec/fixtures/rails4_users_app/config/boot.rb +3 -0
  108. data/spec/fixtures/rails4_users_app/config/database.yml +17 -0
  109. data/spec/fixtures/rails4_users_app/config/environment.rb +5 -0
  110. data/spec/fixtures/rails4_users_app/config/environments/development.rb +41 -0
  111. data/spec/fixtures/rails4_users_app/config/environments/production.rb +79 -0
  112. data/spec/fixtures/rails4_users_app/config/environments/test.rb +42 -0
  113. data/spec/fixtures/rails4_users_app/config/initializers/assets.rb +11 -0
  114. data/spec/fixtures/rails4_users_app/config/initializers/backtrace_silencers.rb +7 -0
  115. data/spec/fixtures/rails4_users_app/config/initializers/cookies_serializer.rb +3 -0
  116. data/spec/fixtures/rails4_users_app/config/initializers/filter_parameter_logging.rb +4 -0
  117. data/spec/fixtures/rails4_users_app/config/initializers/inflections.rb +16 -0
  118. data/spec/fixtures/rails4_users_app/config/initializers/mime_types.rb +4 -0
  119. data/spec/fixtures/rails4_users_app/config/initializers/session_store.rb +3 -0
  120. data/spec/fixtures/rails4_users_app/config/initializers/to_time_preserves_timezone.rb +10 -0
  121. data/spec/fixtures/rails4_users_app/config/initializers/wrap_parameters.rb +14 -0
  122. data/spec/fixtures/rails4_users_app/config/locales/en.yml +23 -0
  123. data/spec/fixtures/rails4_users_app/config/routes.rb +12 -0
  124. data/spec/fixtures/rails4_users_app/config/secrets.yml +22 -0
  125. data/spec/fixtures/rails4_users_app/config.ru +4 -0
  126. data/spec/fixtures/rails4_users_app/create_app +23 -0
  127. data/spec/fixtures/rails4_users_app/db/migrate/20191127112304_create_users.rb +10 -0
  128. data/spec/fixtures/rails4_users_app/db/schema.rb +26 -0
  129. data/spec/fixtures/rails4_users_app/db/seeds.rb +7 -0
  130. data/spec/fixtures/rails4_users_app/docker-compose.yml +24 -0
  131. data/spec/fixtures/rails4_users_app/lib/assets/.keep +0 -0
  132. data/spec/fixtures/rails4_users_app/lib/tasks/.keep +0 -0
  133. data/spec/fixtures/rails4_users_app/log/.keep +0 -0
  134. data/spec/fixtures/rails4_users_app/public/404.html +67 -0
  135. data/spec/fixtures/rails4_users_app/public/422.html +67 -0
  136. data/spec/fixtures/rails4_users_app/public/500.html +66 -0
  137. data/spec/fixtures/rails4_users_app/public/favicon.ico +0 -0
  138. data/spec/fixtures/rails4_users_app/public/robots.txt +5 -0
  139. data/spec/fixtures/rails4_users_app/spec/controllers/users_controller_api_spec.rb +49 -0
  140. data/spec/fixtures/rails4_users_app/spec/rails_helper.rb +95 -0
  141. data/spec/fixtures/rails4_users_app/spec/spec_helper.rb +96 -0
  142. data/spec/fixtures/rails4_users_app/test/fixtures/users.yml +9 -0
  143. data/spec/fixtures/rails_users_app/.dockerignore +1 -0
  144. data/spec/fixtures/rails_users_app/.gitignore +39 -0
  145. data/spec/fixtures/rails_users_app/.rspec +1 -0
  146. data/spec/fixtures/rails_users_app/.ruby-version +1 -0
  147. data/spec/fixtures/rails_users_app/Dockerfile +29 -0
  148. data/spec/fixtures/rails_users_app/Dockerfile.pg +3 -0
  149. data/spec/fixtures/rails_users_app/Gemfile +51 -0
  150. data/spec/fixtures/rails_users_app/Rakefile +6 -0
  151. data/spec/fixtures/rails_users_app/app/controllers/api/users_controller.rb +27 -0
  152. data/spec/fixtures/rails_users_app/app/controllers/application_controller.rb +2 -0
  153. data/spec/fixtures/rails_users_app/app/controllers/concerns/.keep +0 -0
  154. data/spec/fixtures/rails_users_app/app/controllers/health_controller.rb +5 -0
  155. data/spec/fixtures/rails_users_app/app/controllers/users_controller.rb +5 -0
  156. data/spec/fixtures/rails_users_app/app/models/activerecord/user.rb +18 -0
  157. data/spec/fixtures/rails_users_app/app/models/concerns/.keep +0 -0
  158. data/spec/fixtures/rails_users_app/app/models/sequel/user.rb +25 -0
  159. data/spec/fixtures/rails_users_app/app/views/layouts/application.html.haml +7 -0
  160. data/spec/fixtures/rails_users_app/app/views/users/index.html.haml +7 -0
  161. data/spec/fixtures/rails_users_app/appmap.yml +3 -0
  162. data/spec/fixtures/rails_users_app/bin/_appmap-record-self +29 -0
  163. data/spec/fixtures/rails_users_app/bin/appmap +29 -0
  164. data/spec/fixtures/rails_users_app/bin/byebug +29 -0
  165. data/spec/fixtures/rails_users_app/bin/gli +29 -0
  166. data/spec/fixtures/rails_users_app/bin/htmldiff +29 -0
  167. data/spec/fixtures/rails_users_app/bin/ldiff +29 -0
  168. data/spec/fixtures/rails_users_app/bin/nokogiri +29 -0
  169. data/spec/fixtures/rails_users_app/bin/rackup +29 -0
  170. data/spec/fixtures/rails_users_app/bin/rails +4 -0
  171. data/spec/fixtures/rails_users_app/bin/rake +29 -0
  172. data/spec/fixtures/rails_users_app/bin/rspec +29 -0
  173. data/spec/fixtures/rails_users_app/bin/ruby-parse +29 -0
  174. data/spec/fixtures/rails_users_app/bin/ruby-rewrite +29 -0
  175. data/spec/fixtures/rails_users_app/bin/sequel +29 -0
  176. data/spec/fixtures/rails_users_app/bin/setup +25 -0
  177. data/spec/fixtures/rails_users_app/bin/sprockets +29 -0
  178. data/spec/fixtures/rails_users_app/bin/thor +29 -0
  179. data/spec/fixtures/rails_users_app/bin/update +25 -0
  180. data/spec/fixtures/rails_users_app/config/application.rb +51 -0
  181. data/spec/fixtures/rails_users_app/config/boot.rb +3 -0
  182. data/spec/fixtures/rails_users_app/config/credentials.yml.enc +1 -0
  183. data/spec/fixtures/rails_users_app/config/database.yml +17 -0
  184. data/spec/fixtures/rails_users_app/config/environment.rb +5 -0
  185. data/spec/fixtures/rails_users_app/config/environments/development.rb +40 -0
  186. data/spec/fixtures/rails_users_app/config/environments/production.rb +68 -0
  187. data/spec/fixtures/rails_users_app/config/environments/test.rb +36 -0
  188. data/spec/fixtures/rails_users_app/config/initializers/application_controller_renderer.rb +8 -0
  189. data/spec/fixtures/rails_users_app/config/initializers/backtrace_silencers.rb +7 -0
  190. data/spec/fixtures/rails_users_app/config/initializers/cors.rb +16 -0
  191. data/spec/fixtures/rails_users_app/config/initializers/filter_parameter_logging.rb +4 -0
  192. data/spec/fixtures/rails_users_app/config/initializers/inflections.rb +16 -0
  193. data/spec/fixtures/rails_users_app/config/initializers/mime_types.rb +4 -0
  194. data/spec/fixtures/rails_users_app/config/initializers/record_button.rb +3 -0
  195. data/spec/fixtures/rails_users_app/config/initializers/wrap_parameters.rb +9 -0
  196. data/spec/fixtures/rails_users_app/config/locales/en.yml +33 -0
  197. data/spec/fixtures/rails_users_app/config/routes.rb +11 -0
  198. data/spec/fixtures/rails_users_app/config.ru +5 -0
  199. data/spec/fixtures/rails_users_app/create_app +11 -0
  200. data/spec/fixtures/rails_users_app/db/migrate/20190728211408_create_users.rb +9 -0
  201. data/spec/fixtures/rails_users_app/db/schema.rb +23 -0
  202. data/spec/fixtures/rails_users_app/docker-compose.yml +24 -0
  203. data/spec/fixtures/rails_users_app/lib/tasks/.keep +0 -0
  204. data/spec/fixtures/rails_users_app/log/.keep +0 -0
  205. data/spec/fixtures/rails_users_app/public/robots.txt +1 -0
  206. data/spec/fixtures/rails_users_app/spec/controllers/users_controller_api_spec.rb +29 -0
  207. data/spec/fixtures/rails_users_app/spec/models/user_spec.rb +39 -0
  208. data/spec/fixtures/rails_users_app/spec/rails_helper.rb +66 -0
  209. data/spec/fixtures/rails_users_app/spec/spec_helper.rb +96 -0
  210. data/spec/fixtures/rails_users_app/users_app/.gitignore +20 -0
  211. data/spec/rack_handler_webrick_spec.rb +59 -0
  212. data/spec/rails_spec_helper.rb +34 -0
  213. data/spec/railtie_spec.rb +35 -0
  214. data/spec/record_sql_rails4_pg_spec.rb +76 -0
  215. data/spec/record_sql_rails_pg_spec.rb +68 -0
  216. data/spec/rspec_feature_metadata_spec.rb +30 -0
  217. data/spec/spec_helper.rb +6 -0
  218. data/test/cli_test.rb +81 -0
  219. data/test/config_test.rb +149 -0
  220. data/test/explict_inspect_test.rb +29 -0
  221. data/test/fixtures/active_record_like/active_record/aggregations.rb +4 -0
  222. data/test/fixtures/active_record_like/active_record/association.rb +4 -0
  223. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_base.rb +8 -0
  224. data/test/fixtures/active_record_like/active_record/associations/join_dependency/join_part.rb +8 -0
  225. data/test/fixtures/active_record_like/active_record/associations/join_dependency.rb +6 -0
  226. data/test/fixtures/active_record_like/active_record/caps/caps.rb +4 -0
  227. data/test/fixtures/active_record_like/active_record.rb +2 -0
  228. data/test/fixtures/cli_record_test/appmap.yml +2 -0
  229. data/test/fixtures/cli_record_test/lib/cli_record_test/main.rb +7 -0
  230. data/test/fixtures/ignore_non_ruby_file/class.rb +3 -0
  231. data/test/fixtures/ignore_non_ruby_file/non-ruby.txt +1 -0
  232. data/test/fixtures/includes_excludes/lib/a/a_1.rb +6 -0
  233. data/test/fixtures/includes_excludes/lib/a/a_2.rb +6 -0
  234. data/test/fixtures/includes_excludes/lib/a/x/x_1.rb +8 -0
  235. data/test/fixtures/includes_excludes/lib/b/b_1.rb +6 -0
  236. data/test/fixtures/includes_excludes/lib/root_1.rb +4 -0
  237. data/test/fixtures/inspect_multiple_subdirs/module_a/class_a.rb +5 -0
  238. data/test/fixtures/inspect_multiple_subdirs/module_a.rb +2 -0
  239. data/test/fixtures/inspect_multiple_subdirs/module_b/class_b.rb +5 -0
  240. data/test/fixtures/inspect_multiple_subdirs/module_b/class_c.rb +5 -0
  241. data/test/fixtures/inspect_multiple_subdirs/module_b.rb +2 -0
  242. data/test/fixtures/inspect_package/module_a/module_b/class_in_module.rb +6 -0
  243. data/test/fixtures/parse_file/defs_static_function.rb +96 -0
  244. data/test/fixtures/parse_file/function_within_class.rb +36 -0
  245. data/test/fixtures/parse_file/include_public_methods.rb +127 -0
  246. data/test/fixtures/parse_file/instance_function.rb +17 -0
  247. data/test/fixtures/parse_file/modules.rb +71 -0
  248. data/test/fixtures/parse_file/sclass_static_function.rb +88 -0
  249. data/test/fixtures/parse_file/toplevel_class.rb +13 -0
  250. data/test/fixtures/parse_file/toplevel_function.rb +14 -0
  251. data/test/fixtures/rspec_recorder/Gemfile +5 -0
  252. data/test/fixtures/rspec_recorder/appmap.yml +3 -0
  253. data/test/fixtures/rspec_recorder/lib/hello.rb +5 -0
  254. data/test/fixtures/rspec_recorder/spec/hello_spec.rb +9 -0
  255. data/test/fixtures/trace_test/trace_program_1.rb +44 -0
  256. data/test/implicit_inspect_test.rb +33 -0
  257. data/test/include_exclude_test.rb +48 -0
  258. data/test/prerecorded_trace_test.rb +76 -0
  259. data/test/rspec_test.rb +22 -0
  260. data/test/test_helper.rb +46 -0
  261. data/test/trace_test.rb +92 -0
  262. metadata +501 -0
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AppMap
4
+ VERSION = '0.18.1'
5
+ end