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.
- checksums.yaml +7 -0
- data/.github/ISSUE_TEMPLATE.md +29 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- data/.gitignore +35 -0
- data/.rubocop.yml +109 -0
- data/.simplecov +110 -0
- data/.travis.yml +63 -0
- data/CHANGELOG.md +727 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +105 -0
- data/Gemfile +74 -0
- data/MIT-LICENSE +22 -0
- data/README.md +305 -0
- data/Rakefile +76 -0
- data/active_model_serializers.gemspec +64 -0
- data/appveyor.yml +28 -0
- data/bin/bench +171 -0
- data/bin/bench_regression +316 -0
- data/bin/rubocop +38 -0
- data/bin/serve_benchmark +39 -0
- data/docs/README.md +41 -0
- data/docs/STYLE.md +58 -0
- data/docs/general/adapters.md +269 -0
- data/docs/general/caching.md +58 -0
- data/docs/general/configuration_options.md +185 -0
- data/docs/general/deserialization.md +100 -0
- data/docs/general/fields.md +31 -0
- data/docs/general/getting_started.md +133 -0
- data/docs/general/instrumentation.md +40 -0
- data/docs/general/key_transforms.md +40 -0
- data/docs/general/logging.md +21 -0
- data/docs/general/rendering.md +293 -0
- data/docs/general/serializers.md +495 -0
- data/docs/how-open-source-maintained.jpg +0 -0
- data/docs/howto/add_pagination_links.md +138 -0
- data/docs/howto/add_relationship_links.md +140 -0
- data/docs/howto/add_root_key.md +62 -0
- data/docs/howto/grape_integration.md +42 -0
- data/docs/howto/outside_controller_use.md +66 -0
- data/docs/howto/passing_arbitrary_options.md +27 -0
- data/docs/howto/serialize_poro.md +73 -0
- data/docs/howto/test.md +154 -0
- data/docs/howto/upgrade_from_0_8_to_0_10.md +265 -0
- data/docs/integrations/ember-and-json-api.md +147 -0
- data/docs/integrations/grape.md +19 -0
- data/docs/jsonapi/errors.md +56 -0
- data/docs/jsonapi/schema.md +151 -0
- data/docs/jsonapi/schema/schema.json +366 -0
- data/docs/rfcs/0000-namespace.md +106 -0
- data/docs/rfcs/template.md +15 -0
- data/lib/action_controller/serialization.rb +76 -0
- data/lib/active_model/serializable_resource.rb +13 -0
- data/lib/active_model/serializer.rb +418 -0
- data/lib/active_model/serializer/adapter.rb +26 -0
- data/lib/active_model/serializer/adapter/attributes.rb +17 -0
- data/lib/active_model/serializer/adapter/base.rb +20 -0
- data/lib/active_model/serializer/adapter/json.rb +17 -0
- data/lib/active_model/serializer/adapter/json_api.rb +17 -0
- data/lib/active_model/serializer/adapter/null.rb +17 -0
- data/lib/active_model/serializer/array_serializer.rb +14 -0
- data/lib/active_model/serializer/association.rb +91 -0
- data/lib/active_model/serializer/attribute.rb +27 -0
- data/lib/active_model/serializer/belongs_to_reflection.rb +13 -0
- data/lib/active_model/serializer/collection_serializer.rb +90 -0
- data/lib/active_model/serializer/concerns/caching.rb +304 -0
- data/lib/active_model/serializer/error_serializer.rb +16 -0
- data/lib/active_model/serializer/errors_serializer.rb +34 -0
- data/lib/active_model/serializer/field.rb +92 -0
- data/lib/active_model/serializer/fieldset.rb +33 -0
- data/lib/active_model/serializer/has_many_reflection.rb +12 -0
- data/lib/active_model/serializer/has_one_reflection.rb +9 -0
- data/lib/active_model/serializer/lazy_association.rb +99 -0
- data/lib/active_model/serializer/link.rb +23 -0
- data/lib/active_model/serializer/lint.rb +152 -0
- data/lib/active_model/serializer/null.rb +19 -0
- data/lib/active_model/serializer/reflection.rb +212 -0
- data/lib/active_model/serializer/version.rb +7 -0
- data/lib/active_model_serializers.rb +63 -0
- data/lib/active_model_serializers/adapter.rb +100 -0
- data/lib/active_model_serializers/adapter/attributes.rb +15 -0
- data/lib/active_model_serializers/adapter/base.rb +85 -0
- data/lib/active_model_serializers/adapter/json.rb +23 -0
- data/lib/active_model_serializers/adapter/json_api.rb +535 -0
- data/lib/active_model_serializers/adapter/json_api/deserialization.rb +215 -0
- data/lib/active_model_serializers/adapter/json_api/error.rb +98 -0
- data/lib/active_model_serializers/adapter/json_api/jsonapi.rb +51 -0
- data/lib/active_model_serializers/adapter/json_api/link.rb +85 -0
- data/lib/active_model_serializers/adapter/json_api/meta.rb +39 -0
- data/lib/active_model_serializers/adapter/json_api/pagination_links.rb +90 -0
- data/lib/active_model_serializers/adapter/json_api/relationship.rb +106 -0
- data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +68 -0
- data/lib/active_model_serializers/adapter/null.rb +11 -0
- data/lib/active_model_serializers/callbacks.rb +57 -0
- data/lib/active_model_serializers/deprecate.rb +56 -0
- data/lib/active_model_serializers/deserialization.rb +17 -0
- data/lib/active_model_serializers/json_pointer.rb +16 -0
- data/lib/active_model_serializers/logging.rb +124 -0
- data/lib/active_model_serializers/lookup_chain.rb +82 -0
- data/lib/active_model_serializers/model.rb +132 -0
- data/lib/active_model_serializers/railtie.rb +52 -0
- data/lib/active_model_serializers/register_jsonapi_renderer.rb +80 -0
- data/lib/active_model_serializers/serializable_resource.rb +84 -0
- data/lib/active_model_serializers/serialization_context.rb +41 -0
- data/lib/active_model_serializers/test.rb +9 -0
- data/lib/active_model_serializers/test/schema.rb +140 -0
- data/lib/active_model_serializers/test/serializer.rb +127 -0
- data/lib/generators/rails/USAGE +6 -0
- data/lib/generators/rails/resource_override.rb +12 -0
- data/lib/generators/rails/serializer_generator.rb +38 -0
- data/lib/generators/rails/templates/serializer.rb.erb +8 -0
- data/lib/grape/active_model_serializers.rb +18 -0
- data/lib/grape/formatters/active_model_serializers.rb +34 -0
- data/lib/grape/helpers/active_model_serializers.rb +19 -0
- data/lib/tasks/rubocop.rake +55 -0
- data/test/action_controller/adapter_selector_test.rb +64 -0
- data/test/action_controller/explicit_serializer_test.rb +137 -0
- data/test/action_controller/json/include_test.rb +248 -0
- data/test/action_controller/json_api/deserialization_test.rb +114 -0
- data/test/action_controller/json_api/errors_test.rb +42 -0
- data/test/action_controller/json_api/fields_test.rb +68 -0
- data/test/action_controller/json_api/linked_test.rb +204 -0
- data/test/action_controller/json_api/pagination_test.rb +126 -0
- data/test/action_controller/json_api/transform_test.rb +191 -0
- data/test/action_controller/lookup_proc_test.rb +51 -0
- data/test/action_controller/namespace_lookup_test.rb +239 -0
- data/test/action_controller/serialization_scope_name_test.rb +237 -0
- data/test/action_controller/serialization_test.rb +480 -0
- data/test/active_model_serializers/adapter_for_test.rb +210 -0
- data/test/active_model_serializers/json_pointer_test.rb +24 -0
- data/test/active_model_serializers/logging_test.rb +79 -0
- data/test/active_model_serializers/model_test.rb +144 -0
- data/test/active_model_serializers/railtie_test_isolated.rb +70 -0
- data/test/active_model_serializers/register_jsonapi_renderer_test_isolated.rb +163 -0
- data/test/active_model_serializers/serialization_context_test_isolated.rb +73 -0
- data/test/active_model_serializers/test/schema_test.rb +133 -0
- data/test/active_model_serializers/test/serializer_test.rb +64 -0
- data/test/active_record_test.rb +11 -0
- data/test/adapter/attributes_test.rb +42 -0
- data/test/adapter/deprecation_test.rb +102 -0
- data/test/adapter/json/belongs_to_test.rb +47 -0
- data/test/adapter/json/collection_test.rb +106 -0
- data/test/adapter/json/has_many_test.rb +55 -0
- data/test/adapter/json/transform_test.rb +95 -0
- data/test/adapter/json_api/belongs_to_test.rb +157 -0
- data/test/adapter/json_api/collection_test.rb +98 -0
- data/test/adapter/json_api/errors_test.rb +78 -0
- data/test/adapter/json_api/fields_test.rb +98 -0
- data/test/adapter/json_api/has_many_explicit_serializer_test.rb +98 -0
- data/test/adapter/json_api/has_many_test.rb +175 -0
- data/test/adapter/json_api/has_one_test.rb +82 -0
- data/test/adapter/json_api/include_data_if_sideloaded_test.rb +215 -0
- data/test/adapter/json_api/json_api_test.rb +35 -0
- data/test/adapter/json_api/linked_test.rb +415 -0
- data/test/adapter/json_api/links_test.rb +112 -0
- data/test/adapter/json_api/pagination_links_test.rb +208 -0
- data/test/adapter/json_api/parse_test.rb +139 -0
- data/test/adapter/json_api/relationship_test.rb +399 -0
- data/test/adapter/json_api/resource_meta_test.rb +102 -0
- data/test/adapter/json_api/toplevel_jsonapi_test.rb +84 -0
- data/test/adapter/json_api/transform_test.rb +514 -0
- data/test/adapter/json_api/type_test.rb +195 -0
- data/test/adapter/json_test.rb +48 -0
- data/test/adapter/null_test.rb +24 -0
- data/test/adapter/polymorphic_test.rb +220 -0
- data/test/adapter_test.rb +69 -0
- data/test/array_serializer_test.rb +24 -0
- data/test/benchmark/app.rb +67 -0
- data/test/benchmark/benchmarking_support.rb +69 -0
- data/test/benchmark/bm_active_record.rb +83 -0
- data/test/benchmark/bm_adapter.rb +40 -0
- data/test/benchmark/bm_caching.rb +121 -0
- data/test/benchmark/bm_lookup_chain.rb +85 -0
- data/test/benchmark/bm_transform.rb +47 -0
- data/test/benchmark/config.ru +3 -0
- data/test/benchmark/controllers.rb +85 -0
- data/test/benchmark/fixtures.rb +221 -0
- data/test/cache_test.rb +717 -0
- data/test/collection_serializer_test.rb +129 -0
- data/test/fixtures/active_record.rb +115 -0
- data/test/fixtures/poro.rb +227 -0
- data/test/generators/scaffold_controller_generator_test.rb +26 -0
- data/test/generators/serializer_generator_test.rb +77 -0
- data/test/grape_test.rb +198 -0
- data/test/lint_test.rb +51 -0
- data/test/logger_test.rb +22 -0
- data/test/poro_test.rb +11 -0
- data/test/serializable_resource_test.rb +81 -0
- data/test/serializers/association_macros_test.rb +39 -0
- data/test/serializers/associations_test.rb +520 -0
- data/test/serializers/attribute_test.rb +155 -0
- data/test/serializers/attributes_test.rb +54 -0
- data/test/serializers/caching_configuration_test_isolated.rb +172 -0
- data/test/serializers/configuration_test.rb +34 -0
- data/test/serializers/fieldset_test.rb +16 -0
- data/test/serializers/meta_test.rb +204 -0
- data/test/serializers/options_test.rb +34 -0
- data/test/serializers/read_attribute_for_serialization_test.rb +81 -0
- data/test/serializers/reflection_test.rb +481 -0
- data/test/serializers/root_test.rb +23 -0
- data/test/serializers/serialization_test.rb +57 -0
- data/test/serializers/serializer_for_test.rb +138 -0
- data/test/serializers/serializer_for_with_namespace_test.rb +90 -0
- data/test/support/custom_schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
- data/test/support/isolated_unit.rb +86 -0
- data/test/support/rails5_shims.rb +55 -0
- data/test/support/rails_app.rb +40 -0
- data/test/support/schemas/active_model_serializers/test/schema_test/my/index.json +6 -0
- data/test/support/schemas/active_model_serializers/test/schema_test/my/show.json +6 -0
- data/test/support/schemas/custom/show.json +7 -0
- data/test/support/schemas/hyper_schema.json +93 -0
- data/test/support/schemas/render_using_json_api.json +43 -0
- data/test/support/schemas/simple_json_pointers.json +10 -0
- data/test/support/serialization_testing.rb +81 -0
- data/test/test_helper.rb +72 -0
- metadata +622 -0
data/Rakefile
ADDED
|
@@ -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
|
data/appveyor.yml
ADDED
|
@@ -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
|
data/bin/bench
ADDED
|
@@ -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
|