appmap 0.18.1

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