active_model_serializers_custom 0.10.90

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 (215) hide show
  1. checksums.yaml +7 -0
  2. data/.github/ISSUE_TEMPLATE.md +29 -0
  3. data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  4. data/.gitignore +35 -0
  5. data/.rubocop.yml +109 -0
  6. data/.simplecov +110 -0
  7. data/.travis.yml +63 -0
  8. data/CHANGELOG.md +727 -0
  9. data/CODE_OF_CONDUCT.md +74 -0
  10. data/CONTRIBUTING.md +105 -0
  11. data/Gemfile +74 -0
  12. data/MIT-LICENSE +22 -0
  13. data/README.md +305 -0
  14. data/Rakefile +76 -0
  15. data/active_model_serializers.gemspec +64 -0
  16. data/appveyor.yml +28 -0
  17. data/bin/bench +171 -0
  18. data/bin/bench_regression +316 -0
  19. data/bin/rubocop +38 -0
  20. data/bin/serve_benchmark +39 -0
  21. data/docs/README.md +41 -0
  22. data/docs/STYLE.md +58 -0
  23. data/docs/general/adapters.md +269 -0
  24. data/docs/general/caching.md +58 -0
  25. data/docs/general/configuration_options.md +185 -0
  26. data/docs/general/deserialization.md +100 -0
  27. data/docs/general/fields.md +31 -0
  28. data/docs/general/getting_started.md +133 -0
  29. data/docs/general/instrumentation.md +40 -0
  30. data/docs/general/key_transforms.md +40 -0
  31. data/docs/general/logging.md +21 -0
  32. data/docs/general/rendering.md +293 -0
  33. data/docs/general/serializers.md +495 -0
  34. data/docs/how-open-source-maintained.jpg +0 -0
  35. data/docs/howto/add_pagination_links.md +138 -0
  36. data/docs/howto/add_relationship_links.md +140 -0
  37. data/docs/howto/add_root_key.md +62 -0
  38. data/docs/howto/grape_integration.md +42 -0
  39. data/docs/howto/outside_controller_use.md +66 -0
  40. data/docs/howto/passing_arbitrary_options.md +27 -0
  41. data/docs/howto/serialize_poro.md +73 -0
  42. data/docs/howto/test.md +154 -0
  43. data/docs/howto/upgrade_from_0_8_to_0_10.md +265 -0
  44. data/docs/integrations/ember-and-json-api.md +147 -0
  45. data/docs/integrations/grape.md +19 -0
  46. data/docs/jsonapi/errors.md +56 -0
  47. data/docs/jsonapi/schema.md +151 -0
  48. data/docs/jsonapi/schema/schema.json +366 -0
  49. data/docs/rfcs/0000-namespace.md +106 -0
  50. data/docs/rfcs/template.md +15 -0
  51. data/lib/action_controller/serialization.rb +76 -0
  52. data/lib/active_model/serializable_resource.rb +13 -0
  53. data/lib/active_model/serializer.rb +418 -0
  54. data/lib/active_model/serializer/adapter.rb +26 -0
  55. data/lib/active_model/serializer/adapter/attributes.rb +17 -0
  56. data/lib/active_model/serializer/adapter/base.rb +20 -0
  57. data/lib/active_model/serializer/adapter/json.rb +17 -0
  58. data/lib/active_model/serializer/adapter/json_api.rb +17 -0
  59. data/lib/active_model/serializer/adapter/null.rb +17 -0
  60. data/lib/active_model/serializer/array_serializer.rb +14 -0
  61. data/lib/active_model/serializer/association.rb +91 -0
  62. data/lib/active_model/serializer/attribute.rb +27 -0
  63. data/lib/active_model/serializer/belongs_to_reflection.rb +13 -0
  64. data/lib/active_model/serializer/collection_serializer.rb +90 -0
  65. data/lib/active_model/serializer/concerns/caching.rb +304 -0
  66. data/lib/active_model/serializer/error_serializer.rb +16 -0
  67. data/lib/active_model/serializer/errors_serializer.rb +34 -0
  68. data/lib/active_model/serializer/field.rb +92 -0
  69. data/lib/active_model/serializer/fieldset.rb +33 -0
  70. data/lib/active_model/serializer/has_many_reflection.rb +12 -0
  71. data/lib/active_model/serializer/has_one_reflection.rb +9 -0
  72. data/lib/active_model/serializer/lazy_association.rb +99 -0
  73. data/lib/active_model/serializer/link.rb +23 -0
  74. data/lib/active_model/serializer/lint.rb +152 -0
  75. data/lib/active_model/serializer/null.rb +19 -0
  76. data/lib/active_model/serializer/reflection.rb +212 -0
  77. data/lib/active_model/serializer/version.rb +7 -0
  78. data/lib/active_model_serializers.rb +63 -0
  79. data/lib/active_model_serializers/adapter.rb +100 -0
  80. data/lib/active_model_serializers/adapter/attributes.rb +15 -0
  81. data/lib/active_model_serializers/adapter/base.rb +85 -0
  82. data/lib/active_model_serializers/adapter/json.rb +23 -0
  83. data/lib/active_model_serializers/adapter/json_api.rb +535 -0
  84. data/lib/active_model_serializers/adapter/json_api/deserialization.rb +215 -0
  85. data/lib/active_model_serializers/adapter/json_api/error.rb +98 -0
  86. data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +51 -0
  87. data/lib/active_model_serializers/adapter/json_api/link.rb +85 -0
  88. data/lib/active_model_serializers/adapter/json_api/meta.rb +39 -0
  89. data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +90 -0
  90. data/lib/active_model_serializers/adapter/json_api/relationship.rb +106 -0
  91. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +68 -0
  92. data/lib/active_model_serializers/adapter/null.rb +11 -0
  93. data/lib/active_model_serializers/callbacks.rb +57 -0
  94. data/lib/active_model_serializers/deprecate.rb +56 -0
  95. data/lib/active_model_serializers/deserialization.rb +17 -0
  96. data/lib/active_model_serializers/json_pointer.rb +16 -0
  97. data/lib/active_model_serializers/logging.rb +124 -0
  98. data/lib/active_model_serializers/lookup_chain.rb +82 -0
  99. data/lib/active_model_serializers/model.rb +132 -0
  100. data/lib/active_model_serializers/railtie.rb +52 -0
  101. data/lib/active_model_serializers/register_jsonapi_renderer.rb +80 -0
  102. data/lib/active_model_serializers/serializable_resource.rb +84 -0
  103. data/lib/active_model_serializers/serialization_context.rb +41 -0
  104. data/lib/active_model_serializers/test.rb +9 -0
  105. data/lib/active_model_serializers/test/schema.rb +140 -0
  106. data/lib/active_model_serializers/test/serializer.rb +127 -0
  107. data/lib/generators/rails/USAGE +6 -0
  108. data/lib/generators/rails/resource_override.rb +12 -0
  109. data/lib/generators/rails/serializer_generator.rb +38 -0
  110. data/lib/generators/rails/templates/serializer.rb.erb +8 -0
  111. data/lib/grape/active_model_serializers.rb +18 -0
  112. data/lib/grape/formatters/active_model_serializers.rb +34 -0
  113. data/lib/grape/helpers/active_model_serializers.rb +19 -0
  114. data/lib/tasks/rubocop.rake +55 -0
  115. data/test/action_controller/adapter_selector_test.rb +64 -0
  116. data/test/action_controller/explicit_serializer_test.rb +137 -0
  117. data/test/action_controller/json/include_test.rb +248 -0
  118. data/test/action_controller/json_api/deserialization_test.rb +114 -0
  119. data/test/action_controller/json_api/errors_test.rb +42 -0
  120. data/test/action_controller/json_api/fields_test.rb +68 -0
  121. data/test/action_controller/json_api/linked_test.rb +204 -0
  122. data/test/action_controller/json_api/pagination_test.rb +126 -0
  123. data/test/action_controller/json_api/transform_test.rb +191 -0
  124. data/test/action_controller/lookup_proc_test.rb +51 -0
  125. data/test/action_controller/namespace_lookup_test.rb +239 -0
  126. data/test/action_controller/serialization_scope_name_test.rb +237 -0
  127. data/test/action_controller/serialization_test.rb +480 -0
  128. data/test/active_model_serializers/adapter_for_test.rb +210 -0
  129. data/test/active_model_serializers/json_pointer_test.rb +24 -0
  130. data/test/active_model_serializers/logging_test.rb +79 -0
  131. data/test/active_model_serializers/model_test.rb +144 -0
  132. data/test/active_model_serializers/railtie_test_isolated.rb +70 -0
  133. data/test/active_model_serializers/register_jsonapi_renderer_test_isolated.rb +163 -0
  134. data/test/active_model_serializers/serialization_context_test_isolated.rb +73 -0
  135. data/test/active_model_serializers/test/schema_test.rb +133 -0
  136. data/test/active_model_serializers/test/serializer_test.rb +64 -0
  137. data/test/active_record_test.rb +11 -0
  138. data/test/adapter/attributes_test.rb +42 -0
  139. data/test/adapter/deprecation_test.rb +102 -0
  140. data/test/adapter/json/belongs_to_test.rb +47 -0
  141. data/test/adapter/json/collection_test.rb +106 -0
  142. data/test/adapter/json/has_many_test.rb +55 -0
  143. data/test/adapter/json/transform_test.rb +95 -0
  144. data/test/adapter/json_api/belongs_to_test.rb +157 -0
  145. data/test/adapter/json_api/collection_test.rb +98 -0
  146. data/test/adapter/json_api/errors_test.rb +78 -0
  147. data/test/adapter/json_api/fields_test.rb +98 -0
  148. data/test/adapter/json_api/has_many_explicit_serializer_test.rb +98 -0
  149. data/test/adapter/json_api/has_many_test.rb +175 -0
  150. data/test/adapter/json_api/has_one_test.rb +82 -0
  151. data/test/adapter/json_api/include_data_if_sideloaded_test.rb +215 -0
  152. data/test/adapter/json_api/json_api_test.rb +35 -0
  153. data/test/adapter/json_api/linked_test.rb +415 -0
  154. data/test/adapter/json_api/links_test.rb +112 -0
  155. data/test/adapter/json_api/pagination_links_test.rb +208 -0
  156. data/test/adapter/json_api/parse_test.rb +139 -0
  157. data/test/adapter/json_api/relationship_test.rb +399 -0
  158. data/test/adapter/json_api/resource_meta_test.rb +102 -0
  159. data/test/adapter/json_api/toplevel_jsonapi_test.rb +84 -0
  160. data/test/adapter/json_api/transform_test.rb +514 -0
  161. data/test/adapter/json_api/type_test.rb +195 -0
  162. data/test/adapter/json_test.rb +48 -0
  163. data/test/adapter/null_test.rb +24 -0
  164. data/test/adapter/polymorphic_test.rb +220 -0
  165. data/test/adapter_test.rb +69 -0
  166. data/test/array_serializer_test.rb +24 -0
  167. data/test/benchmark/app.rb +67 -0
  168. data/test/benchmark/benchmarking_support.rb +69 -0
  169. data/test/benchmark/bm_active_record.rb +83 -0
  170. data/test/benchmark/bm_adapter.rb +40 -0
  171. data/test/benchmark/bm_caching.rb +121 -0
  172. data/test/benchmark/bm_lookup_chain.rb +85 -0
  173. data/test/benchmark/bm_transform.rb +47 -0
  174. data/test/benchmark/config.ru +3 -0
  175. data/test/benchmark/controllers.rb +85 -0
  176. data/test/benchmark/fixtures.rb +221 -0
  177. data/test/cache_test.rb +717 -0
  178. data/test/collection_serializer_test.rb +129 -0
  179. data/test/fixtures/active_record.rb +115 -0
  180. data/test/fixtures/poro.rb +227 -0
  181. data/test/generators/scaffold_controller_generator_test.rb +26 -0
  182. data/test/generators/serializer_generator_test.rb +77 -0
  183. data/test/grape_test.rb +198 -0
  184. data/test/lint_test.rb +51 -0
  185. data/test/logger_test.rb +22 -0
  186. data/test/poro_test.rb +11 -0
  187. data/test/serializable_resource_test.rb +81 -0
  188. data/test/serializers/association_macros_test.rb +39 -0
  189. data/test/serializers/associations_test.rb +520 -0
  190. data/test/serializers/attribute_test.rb +155 -0
  191. data/test/serializers/attributes_test.rb +54 -0
  192. data/test/serializers/caching_configuration_test_isolated.rb +172 -0
  193. data/test/serializers/configuration_test.rb +34 -0
  194. data/test/serializers/fieldset_test.rb +16 -0
  195. data/test/serializers/meta_test.rb +204 -0
  196. data/test/serializers/options_test.rb +34 -0
  197. data/test/serializers/read_attribute_for_serialization_test.rb +81 -0
  198. data/test/serializers/reflection_test.rb +481 -0
  199. data/test/serializers/root_test.rb +23 -0
  200. data/test/serializers/serialization_test.rb +57 -0
  201. data/test/serializers/serializer_for_test.rb +138 -0
  202. data/test/serializers/serializer_for_with_namespace_test.rb +90 -0
  203. data/test/support/custom_schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
  204. data/test/support/isolated_unit.rb +86 -0
  205. data/test/support/rails5_shims.rb +55 -0
  206. data/test/support/rails_app.rb +40 -0
  207. data/test/support/schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
  208. data/test/support/schemas/active_model_serializers/test/schema_test/my/show.json +6 -0
  209. data/test/support/schemas/custom/show.json +7 -0
  210. data/test/support/schemas/hyper_schema.json +93 -0
  211. data/test/support/schemas/render_using_json_api.json +43 -0
  212. data/test/support/schemas/simple_json_pointers.json +10 -0
  213. data/test/support/serialization_testing.rb +81 -0
  214. data/test/test_helper.rb +72 -0
  215. metadata +622 -0
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+ begin
9
+ require 'simplecov'
10
+ rescue LoadError # rubocop:disable Lint/HandleExceptions
11
+ end
12
+ import('lib/tasks/rubocop.rake')
13
+
14
+ Bundler::GemHelper.install_tasks
15
+
16
+ require 'yard'
17
+
18
+ namespace :yard do
19
+ YARD::Rake::YardocTask.new(:doc) do |t|
20
+ t.stats_options = ['--list-undoc']
21
+ end
22
+
23
+ desc 'start a gem server'
24
+ task :server do
25
+ sh 'bundle exec yard server --gems'
26
+ end
27
+
28
+ desc 'use Graphviz to generate dot graph'
29
+ task :graph do
30
+ output_file = 'doc/erd.dot'
31
+ sh "bundle exec yard graph --protected --full --dependencies > #{output_file}"
32
+ puts 'open doc/erd.dot if you have graphviz installed'
33
+ end
34
+ end
35
+
36
+ require 'rake/testtask'
37
+
38
+ Rake::TestTask.new(:test) do |t|
39
+ t.libs << 'lib'
40
+ t.libs << 'test'
41
+ t.pattern = 'test/**/*_test.rb'
42
+ t.ruby_opts = ['-r./test/test_helper.rb']
43
+ t.ruby_opts << ' -w' unless ENV['NO_WARN'] == 'true'
44
+ t.verbose = true
45
+ end
46
+
47
+ desc 'Run isolated tests'
48
+ task isolated: ['test:isolated']
49
+ namespace :test do
50
+ task :isolated do
51
+ desc 'Run isolated tests for Railtie'
52
+ require 'shellwords'
53
+ dir = File.dirname(__FILE__)
54
+ dir = Shellwords.shellescape(dir)
55
+ isolated_test_files = FileList['test/**/*_test_isolated.rb']
56
+ # https://github.com/rails/rails/blob/3d590add45/railties/lib/rails/generators/app_base.rb#L345-L363
57
+ _bundle_command = Gem.bin_path('bundler', 'bundle')
58
+ require 'bundler'
59
+ Bundler.with_clean_env do
60
+ isolated_test_files.all? do |test_file|
61
+ command = "-w -I#{dir}/lib -I#{dir}/test #{Shellwords.shellescape(test_file)}"
62
+ full_command = %("#{Gem.ruby}" #{command})
63
+ system(full_command)
64
+ end or fail 'Failures' # rubocop:disable Style/AndOr
65
+ end
66
+ end
67
+ end
68
+
69
+ if ENV['RAILS_VERSION'].to_s > '4.0' && RUBY_ENGINE == 'ruby'
70
+ task default: [:isolated, :test, :rubocop]
71
+ else
72
+ task default: [:test, :rubocop]
73
+ end
74
+
75
+ desc 'CI test task'
76
+ task ci: [:default]
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ # coding: utf-8
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'active_model/serializer/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'active_model_serializers_custom'
9
+ spec.version = ActiveModel::Serializer::VERSION
10
+ spec.platform = Gem::Platform::RUBY
11
+ spec.authors = ['Steve Klabnik']
12
+ spec.email = ['steve@steveklabnik.com']
13
+ spec.summary = 'Conventions-based JSON generation for Rails.'
14
+ spec.description = 'ActiveModel::Serializers allows you to generate your JSON in an object-oriented and convention-driven manner.'
15
+ spec.homepage = 'https://github.com/rails-api/active_model_serializers'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+ spec.executables = []
22
+
23
+ spec.required_ruby_version = '>= 2.1'
24
+
25
+ rails_versions = ['>= 4.1', '< 6']
26
+ spec.add_runtime_dependency 'activemodel', rails_versions
27
+ # 'activesupport', rails_versions
28
+ # 'builder'
29
+
30
+ spec.add_runtime_dependency 'actionpack', rails_versions
31
+ # 'activesupport', rails_versions
32
+ # 'rack'
33
+ # 'rack-test', '~> 0.6.2'
34
+
35
+ spec.add_development_dependency 'railties', rails_versions
36
+ # 'activesupport', rails_versions
37
+ # 'actionpack', rails_versions
38
+ # 'rake', '>= 0.8.7'
39
+
40
+ # 'activesupport', rails_versions
41
+ # 'i18n,
42
+ # 'tzinfo'
43
+ spec.add_development_dependency 'minitest', ['~> 5.0', '< 5.11']
44
+ # 'thread_safe'
45
+
46
+ spec.add_runtime_dependency 'jsonapi-renderer', ['>= 0.1.1.beta1', '< 0.3']
47
+ spec.add_runtime_dependency 'case_transform', '>= 0.2'
48
+
49
+ spec.add_development_dependency 'activerecord', rails_versions
50
+ # arel
51
+ # activesupport
52
+ # activemodel
53
+
54
+ # Soft dependency for pagination
55
+ spec.add_development_dependency 'kaminari', ' ~> 0.16.3'
56
+ spec.add_development_dependency 'will_paginate', '~> 3.0', '>= 3.0.7'
57
+
58
+ spec.add_development_dependency 'bundler', '~> 1.6'
59
+ spec.add_development_dependency 'simplecov', '~> 0.11'
60
+ spec.add_development_dependency 'timecop', '~> 0.7'
61
+ spec.add_development_dependency 'grape', ['>= 0.13', '< 0.19.1']
62
+ spec.add_development_dependency 'json_schema'
63
+ spec.add_development_dependency 'rake', ['>= 10.0', '< 12.0']
64
+ end
@@ -0,0 +1,28 @@
1
+ version: 1.0.{build}-{branch}
2
+
3
+ skip_tags: true
4
+
5
+ environment:
6
+ JRUBY_OPTS: "--dev -J-Xmx1024M --debug"
7
+ matrix:
8
+ - ruby_version: "Ruby23"
9
+ - ruby_version: "Ruby23-x64"
10
+
11
+ cache:
12
+ - vendor/bundle
13
+
14
+ install:
15
+ - SET PATH=C:\%ruby_version%\bin;%PATH%
16
+ - bundle env
17
+ - bundle check || bundle install --path=vendor/bundle --retry=3 --jobs=3
18
+ - bundle clean --force
19
+
20
+ before_test:
21
+ - ruby -v
22
+ - gem -v
23
+ - bundle -v
24
+
25
+ test_script:
26
+ - bundle exec rake ci
27
+
28
+ build: off
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env ruby
2
+ # ActiveModelSerializers Benchmark driver
3
+ # Adapted from
4
+ # https://github.com/ruby-bench/ruby-bench-suite/blob/8ad567f7e43a044ae48c36833218423bb1e2bd9d/rails/benchmarks/driver.rb
5
+ require 'bundler'
6
+ Bundler.setup
7
+ require 'json'
8
+ require 'pathname'
9
+ require 'optparse'
10
+ require 'digest'
11
+ require 'pathname'
12
+ require 'shellwords'
13
+ require 'logger'
14
+ require 'English'
15
+
16
+ class BenchmarkDriver
17
+ ROOT = Pathname File.expand_path(File.join('..', '..'), __FILE__)
18
+ BASE = ENV.fetch('BASE') { ROOT.join('test', 'benchmark') }
19
+ ESCAPED_BASE = Shellwords.shellescape(BASE)
20
+
21
+ def self.benchmark(options)
22
+ new(options).run
23
+ end
24
+
25
+ def self.parse_argv_and_run(argv = ARGV, options = {})
26
+ options = {
27
+ repeat_count: 1,
28
+ pattern: [],
29
+ env: 'CACHE_ON=on'
30
+ }.merge!(options)
31
+
32
+ OptionParser.new do |opts|
33
+ opts.banner = 'Usage: bin/bench [options]'
34
+
35
+ opts.on('-r', '--repeat-count [NUM]', 'Run benchmarks [NUM] times taking the best result') do |value|
36
+ options[:repeat_count] = value.to_i
37
+ end
38
+
39
+ opts.on('-p', '--pattern <PATTERN1,PATTERN2,PATTERN3>', 'Benchmark name pattern') do |value|
40
+ options[:pattern] = value.split(',')
41
+ end
42
+
43
+ opts.on('-e', '--env <var1=val1,var2=val2,var3=vale>', 'ENV variables to pass in') do |value|
44
+ options[:env] = value.split(',')
45
+ end
46
+ end.parse!(argv)
47
+
48
+ benchmark(options)
49
+ end
50
+
51
+ attr_reader :commit_hash, :base
52
+
53
+ # Based on logfmt:
54
+ # https://www.brandur.org/logfmt
55
+ # For more complete implementation see:
56
+ # see https://github.com/arachnid-cb/logfmtr/blob/master/lib/logfmtr/base.rb
57
+ # For usage see:
58
+ # https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write/
59
+ # https://engineering.heroku.com/blogs/2014-09-05-hutils-explore-your-structured-data-logs/
60
+ # For Ruby parser see:
61
+ # https://github.com/cyberdelia/logfmt-ruby
62
+ def self.summary_logger(device = 'output.txt')
63
+ require 'time'
64
+ logger = Logger.new(device)
65
+ logger.level = Logger::INFO
66
+ logger.formatter = proc { |severity, datetime, progname, msg|
67
+ msg = "'#{msg}'"
68
+ "level=#{severity} time=#{datetime.utc.iso8601(6)} pid=#{Process.pid} progname=#{progname} msg=#{msg}#{$INPUT_RECORD_SEPARATOR}"
69
+ }
70
+ logger
71
+ end
72
+
73
+ def self.stdout_logger
74
+ logger = Logger.new(STDOUT)
75
+ logger.level = Logger::INFO
76
+ logger.formatter = proc { |_, _, _, msg| "#{msg}#{$INPUT_RECORD_SEPARATOR}" }
77
+ logger
78
+ end
79
+
80
+ def initialize(options)
81
+ @writer = ENV['SUMMARIZE'] ? self.class.summary_logger : self.class.stdout_logger
82
+ @repeat_count = options[:repeat_count]
83
+ @pattern = options[:pattern]
84
+ @commit_hash = options.fetch(:commit_hash) { `git rev-parse --short HEAD`.chomp }
85
+ @base = options.fetch(:base) { ESCAPED_BASE }
86
+ @env = Array(options[:env]).join(' ')
87
+ @rubyopt = options[:rubyopt] # TODO: rename
88
+ end
89
+
90
+ def run
91
+ files.each do |path|
92
+ next if !@pattern.empty? && /#{@pattern.join('|')}/ !~ File.basename(path)
93
+ run_single(Shellwords.shellescape(path))
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ def files
100
+ Dir[File.join(base, 'bm_*')]
101
+ end
102
+
103
+ def run_single(path)
104
+ script = "RAILS_ENV=production #{@env} ruby #{@rubyopt} #{path}"
105
+ environment = `ruby -v`.chomp.strip[/\d+\.\d+\.\d+\w+/]
106
+
107
+ runs_output = measure(script)
108
+ if runs_output.empty?
109
+ results = { error: :no_results }
110
+ return
111
+ end
112
+
113
+ results = {}
114
+ results['commit_hash'] = commit_hash
115
+ results['version'] = runs_output.first['version']
116
+ results['rails_version'] = runs_output.first['rails_version']
117
+ results['benchmark_run[environment]'] = environment
118
+ results['runs'] = []
119
+
120
+ runs_output.each do |output|
121
+ results['runs'] << {
122
+ 'benchmark_type[category]' => output['label'],
123
+ 'benchmark_run[result][iterations_per_second]' => output['iterations_per_second'].round(3),
124
+ 'benchmark_run[result][total_allocated_objects_per_iteration]' => output['total_allocated_objects_per_iteration']
125
+ }
126
+ end
127
+ ensure
128
+ results && report(results)
129
+ end
130
+
131
+ def report(results)
132
+ @writer.info { 'Benchmark results:' }
133
+ @writer.info { JSON.pretty_generate(results) }
134
+ end
135
+
136
+ def summarize(result)
137
+ puts "#{result['label']} #{result['iterations_per_second']}/ips; #{result['total_allocated_objects_per_iteration']} objects"
138
+ end
139
+
140
+ # FIXME: ` provides the full output but it'll return failed output as well.
141
+ def measure(script)
142
+ results = Hash.new { |h, k| h[k] = [] }
143
+
144
+ @repeat_count.times do
145
+ output = sh(script)
146
+ output.each_line do |line|
147
+ next if line.nil?
148
+ begin
149
+ result = JSON.parse(line)
150
+ rescue JSON::ParserError
151
+ result = { error: line } # rubocop:disable Lint/UselessAssignment
152
+ else
153
+ summarize(result)
154
+ results[result['label']] << result
155
+ end
156
+ end
157
+ end
158
+
159
+ results.map do |_, bm_runs|
160
+ bm_runs.sort_by do |run|
161
+ run['iterations_per_second']
162
+ end.last
163
+ end
164
+ end
165
+
166
+ def sh(cmd)
167
+ `#{cmd}`
168
+ end
169
+ end
170
+
171
+ BenchmarkDriver.parse_argv_and_run if $PROGRAM_NAME == __FILE__
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env ruby
2
+ require 'fileutils'
3
+ require 'pathname'
4
+ require 'shellwords'
5
+ require 'English'
6
+
7
+ ############################
8
+ # USAGE
9
+ #
10
+ # bundle exec bin/bench_regression <ref1> <ref2>
11
+ # <ref1> defaults to the current branch
12
+ # <ref2> defaults to the master branch
13
+ # bundle exec bin/bench_regression current # will run on the current branch
14
+ # bundle exec bin/bench_regression revisions 792fb8a90 master # every revision inclusive
15
+ # bundle exec bin/bench_regression 792fb8a90 master --repeat-count 2 --env CACHE_ON=off
16
+ # bundle exec bin/bench_regression vendor
17
+ ###########################
18
+
19
+ class BenchRegression
20
+ ROOT = Pathname File.expand_path(File.join(*['..', '..']), __FILE__)
21
+ TMP_DIR_NAME = File.join('tmp', 'bench')
22
+ TMP_DIR = File.join(ROOT, TMP_DIR_NAME)
23
+ E_TMP_DIR = Shellwords.shellescape(TMP_DIR)
24
+ load ROOT.join('bin', 'bench')
25
+
26
+ attr_reader :source_stasher
27
+
28
+ def initialize
29
+ @source_stasher = SourceStasher.new
30
+ end
31
+
32
+ class SourceStasher
33
+ attr_reader :gem_require_paths, :gem_paths
34
+ attr_writer :vendor
35
+
36
+ def initialize
37
+ @gem_require_paths = []
38
+ @gem_paths = []
39
+ refresh_temp_dir
40
+ @vendor = false
41
+ end
42
+
43
+ def temp_dir_empty?
44
+ File.directory?(TMP_DIR) &&
45
+ Dir[File.join(TMP_DIR, '*')].none?
46
+ end
47
+
48
+ def empty_temp_dir
49
+ return if @vendor
50
+ return if temp_dir_empty?
51
+ FileUtils.mkdir_p(TMP_DIR)
52
+ Dir[File.join(TMP_DIR, '*')].each do |file|
53
+ if File.directory?(file)
54
+ FileUtils.rm_rf(file)
55
+ else
56
+ FileUtils.rm(file)
57
+ end
58
+ end
59
+ end
60
+
61
+ def fill_temp_dir
62
+ vendor_files(Dir[File.join(ROOT, 'test', 'benchmark', '*.{rb,ru}')])
63
+ # vendor_file(File.join('bin', 'bench'))
64
+ housekeeping { empty_temp_dir }
65
+ vendor_gem('benchmark-ips')
66
+ end
67
+
68
+ def vendor_files(files)
69
+ files.each do |file|
70
+ vendor_file(file)
71
+ end
72
+ end
73
+
74
+ def vendor_file(file)
75
+ FileUtils.cp(file, File.join(TMP_DIR, File.basename(file)))
76
+ end
77
+
78
+ def vendor_gem(gem_name)
79
+ directory_name = `bundle exec gem unpack benchmark-ips --target=#{E_TMP_DIR}`[/benchmark-ips.+\d/]
80
+ gem_paths << File.join(TMP_DIR, directory_name)
81
+ gem_require_paths << File.join(TMP_DIR_NAME, directory_name, 'lib')
82
+ housekeeping { remove_vendored_gems }
83
+ end
84
+
85
+ def remove_vendored_gems
86
+ return if @vendor
87
+ FileUtils.rm_rf(*gem_paths)
88
+ end
89
+
90
+ def refresh_temp_dir
91
+ empty_temp_dir
92
+ fill_temp_dir
93
+ end
94
+
95
+ def housekeeping
96
+ at_exit { yield }
97
+ end
98
+ end
99
+
100
+ module RevisionMethods
101
+ module_function
102
+ def current_branch
103
+ @current_branch ||= `cat .git/HEAD | cut -d/ -f3,4,5`.chomp
104
+ end
105
+
106
+ def current_revision
107
+ `git rev-parse --short HEAD`.chomp
108
+ end
109
+
110
+ def revision_description(rev)
111
+ `git log --oneline -1 #{rev}`.chomp
112
+ end
113
+
114
+ def revisions(start_ref, end_ref)
115
+ cmd = "git rev-list --reverse #{start_ref}..#{end_ref}"
116
+ `#{cmd}`.chomp.split("\n")
117
+ end
118
+
119
+ def checkout_ref(ref)
120
+ `git checkout #{ref}`.chomp
121
+ if $CHILD_STATUS
122
+ STDERR.puts "Checkout failed: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success?
123
+ $CHILD_STATUS.success?
124
+ else
125
+ true
126
+ end
127
+ end
128
+
129
+ def clean_head
130
+ system('git reset --hard --quiet')
131
+ end
132
+ end
133
+ module ShellMethods
134
+
135
+ def sh(cmd)
136
+ puts cmd
137
+ # system(cmd)
138
+ run(cmd)
139
+ # env = {}
140
+ # # out = STDOUT
141
+ # pid = spawn(env, cmd)
142
+ # Process.wait(pid)
143
+ # pid = fork do
144
+ # exec cmd
145
+ # end
146
+ # Process.waitpid2(pid)
147
+ # puts $CHILD_STATUS.exitstatus
148
+ end
149
+
150
+ require 'pty'
151
+ # should consider trapping SIGINT in here
152
+ def run(cmd)
153
+ puts cmd
154
+ child_process = ''
155
+ result = ''
156
+ # http://stackoverflow.com/a/1162850
157
+ # stream output of subprocess
158
+ begin
159
+ PTY.spawn(cmd) do |stdin, _stdout, pid|
160
+ begin
161
+ # Do stuff with the output here. Just printing to show it works
162
+ stdin.each do |line|
163
+ print line
164
+ result << line
165
+ end
166
+ child_process = PTY.check(pid)
167
+ rescue Errno::EIO
168
+ puts 'Errno:EIO error, but this probably just means ' \
169
+ 'that the process has finished giving output'
170
+ end
171
+ end
172
+ rescue PTY::ChildExited
173
+ puts 'The child process exited!'
174
+ end
175
+ unless (child_process && child_process.success?)
176
+ exitstatus = child_process.exitstatus
177
+ puts "FAILED: #{child_process.pid} exited with status #{exitstatus.inspect} due to failed command #{cmd}"
178
+ exit exitstatus || 1
179
+ end
180
+ result
181
+ end
182
+
183
+ def bundle(ref)
184
+ system("rm -f Gemfile.lock")
185
+ # This is absolutely critical for bundling to work
186
+ Bundler.with_clean_env do
187
+ system("bundle check ||
188
+ bundle install --local ||
189
+ bundle install ||
190
+ bundle update")
191
+ end
192
+
193
+ # if $CHILD_STATUS
194
+ # STDERR.puts "Bundle failed at: #{ref}, #{$CHILD_STATUS.exitstatus}" unless $CHILD_STATUS.success?
195
+ # $CHILD_STATUS.success?
196
+ # else
197
+ # false
198
+ # end
199
+ end
200
+ end
201
+ include ShellMethods
202
+ include RevisionMethods
203
+
204
+ def benchmark_refs(ref1: nil, ref2: nil, cmd:)
205
+ checking_out = false
206
+ ref0 = current_branch
207
+ ref1 ||= current_branch
208
+ ref2 ||= 'master'
209
+ p [ref0, ref1, ref2, current_revision]
210
+
211
+ run_benchmark_at_ref(cmd, ref1)
212
+ p [ref0, ref1, ref2, current_revision]
213
+ run_benchmark_at_ref(cmd, ref2)
214
+ p [ref0, ref1, ref2, current_revision]
215
+
216
+ checking_out = true
217
+ checkout_ref(ref0)
218
+ rescue Exception # rubocop:disable Lint/RescueException
219
+ STDERR.puts "[ERROR] #{$!.message}"
220
+ checkout_ref(ref0) unless checking_out
221
+ raise
222
+ end
223
+
224
+ def benchmark_revisions(ref1: nil, ref2: nil, cmd:)
225
+ checking_out = false
226
+ ref0 = current_branch
227
+ ref1 ||= current_branch
228
+ ref2 ||= 'master'
229
+
230
+ revisions(ref1, ref2).each do |rev|
231
+ STDERR.puts "Checking out: #{revision_description(rev)}"
232
+
233
+ run_benchmark_at_ref(cmd, rev)
234
+ clean_head
235
+ end
236
+ checking_out = true
237
+ checkout_ref(ref0)
238
+ rescue Exception # rubocop:disable Lint/RescueException
239
+ STDERR.puts "[ERROR]: #{$!.message}"
240
+ checkout_ref(ref0) unless checking_out
241
+ raise
242
+ end
243
+
244
+ def run_benchmark_at_ref(cmd, ref)
245
+ checkout_ref(ref)
246
+ run_benchmark(cmd, ref)
247
+ end
248
+
249
+ def run_benchmark(cmd, ref = nil)
250
+ ref ||= current_revision
251
+ bundle(ref) &&
252
+ benchmark_tests(cmd, ref)
253
+ end
254
+
255
+ def benchmark_tests(cmd, ref)
256
+ base = E_TMP_DIR
257
+ # cmd.sub('bin/bench', 'tmp/revision_runner/bench')
258
+ # bundle = Gem.bin('bunle'
259
+ # Bundler.with_clean_env(&block)
260
+
261
+ # cmd = Shellwords.shelljoin(cmd)
262
+ # cmd = "COMMIT_HASH=#{ref} BASE=#{base} bundle exec ruby -rbenchmark/ips #{cmd}"
263
+ # Add vendoring benchmark/ips to load path
264
+
265
+ # CURRENT THINKING: IMPORTANT
266
+ # Pass into require statement as RUBYOPTS i.e. via env rather than command line argument
267
+ # otherwise, have a 'fast ams benchmarking' module that extends benchmarkings to add the 'ams'
268
+ # method but doesn't depend on benchmark-ips
269
+ options = {
270
+ commit_hash: ref,
271
+ base: base,
272
+ rubyopt: Shellwords.shellescape("-Ilib:#{source_stasher.gem_require_paths.join(':')}")
273
+ }
274
+ BenchmarkDriver.parse_argv_and_run(ARGV.dup, options)
275
+ end
276
+ end
277
+
278
+ if $PROGRAM_NAME == __FILE__
279
+ benchmarking = BenchRegression.new
280
+
281
+ case ARGV[0]
282
+ when 'current'
283
+ # Run current branch only
284
+
285
+ # super simple command line parsing
286
+ args = ARGV.dup
287
+ _ = args.shift # remove 'current' from args
288
+ cmd = args
289
+ benchmarking.run_benchmark(cmd)
290
+ when 'revisions'
291
+ # Runs on every revision
292
+
293
+ # super simple command line parsing
294
+ args = ARGV.dup
295
+ _ = args.shift
296
+ ref1 = args.shift # remove 'revisions' from args
297
+ ref2 = args.shift
298
+ cmd = args
299
+ benchmarking.benchmark_revisions(ref1: ref1, ref2: ref2, cmd: cmd)
300
+ when 'vendor'
301
+ # Just prevents vendored files from being cleaned up
302
+ # at exit. (They are vendored at initialize.)
303
+ benchmarking.source_stasher.vendor = true
304
+ else
305
+ # Default: Compare current_branch to master
306
+ # Optionally: pass in two refs as args to `bin/bench_regression`
307
+ # TODO: Consider checking across more revisions, to automatically find problems.
308
+
309
+ # super simple command line parsing
310
+ args = ARGV.dup
311
+ ref1 = args.shift
312
+ ref2 = args.shift
313
+ cmd = args
314
+ benchmarking.benchmark_refs(ref1: ref1, ref2: ref2, cmd: cmd)
315
+ end
316
+ end