aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.codeclimate.yml2
-rw-r--r--.gitignore29
-rw-r--r--.rubocop.yml6
-rw-r--r--.travis.yml26
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock41
-rw-r--r--actioncable/.gitignore4
-rw-r--r--actioncable/CHANGELOG.md5
-rw-r--r--actioncable/actioncable.gemspec2
-rw-r--r--actioncable/lib/action_cable/connection/base.rb5
-rw-r--r--actioncable/lib/action_cable/connection/message_buffer.rb5
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb6
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb4
-rw-r--r--actionmailer/CHANGELOG.md5
-rw-r--r--actionmailer/actionmailer.gemspec2
-rw-r--r--actionpack/CHANGELOG.md6
-rw-r--r--actionpack/actionpack.gemspec2
-rw-r--r--actionpack/lib/action_controller/metal/content_security_policy.rb18
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb3
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb34
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/nodes/node.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb4
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb15
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb2
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb9
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb26
-rw-r--r--actionpack/test/controller/parameters/mutators_test.rb1
-rw-r--r--actionpack/test/controller/parameters/serialization_test.rb5
-rw-r--r--actionpack/test/controller/routing_test.rb2
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb80
-rw-r--r--actionpack/test/dispatch/routing_assertions_test.rb6
-rw-r--r--actionpack/test/dispatch/routing_test.rb7
-rw-r--r--actionpack/test/dispatch/ssl_test.rb4
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb10
-rw-r--r--actionpack/test/tmp/.gitignore0
-rw-r--r--actionview/.gitignore7
-rw-r--r--actionview/CHANGELOG.md38
-rw-r--r--actionview/actionview.gemspec2
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee4
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee4
-rw-r--r--actionview/lib/action_view/helpers.rb2
-rw-r--r--actionview/lib/action_view/helpers/csp_helper.rb24
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb17
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb4
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb11
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb6
-rw-r--r--actionview/lib/action_view/helpers/tags/translator.rb7
-rw-r--r--actionview/lib/action_view/railtie.rb10
-rw-r--r--actionview/test/fixtures/public/.gitignore1
-rw-r--r--actionview/test/template/date_helper_test.rb6
-rw-r--r--actionview/test/template/form_helper/form_with_test.rb85
-rw-r--r--actionview/test/template/form_helper_test.rb57
-rw-r--r--actionview/test/template/form_options_helper_test.rb10
-rw-r--r--actionview/test/template/form_tag_helper_test.rb27
-rw-r--r--actionview/test/tmp/.keep0
-rw-r--r--actionview/test/ujs/.gitignore1
-rw-r--r--actionview/test/ujs/public/test/call-ajax.js3
-rw-r--r--actionview/test/ujs/server.rb26
-rw-r--r--actionview/test/ujs/views/layouts/application.html.erb7
-rw-r--r--activejob/CHANGELOG.md17
-rw-r--r--activejob/activejob.gemspec2
-rw-r--r--activejob/lib/active_job.rb1
-rw-r--r--activejob/lib/active_job/arguments.rb34
-rw-r--r--activejob/lib/active_job/base.rb3
-rw-r--r--activejob/lib/active_job/core.rb7
-rw-r--r--activejob/lib/active_job/logging.rb5
-rw-r--r--activejob/lib/active_job/railtie.rb13
-rw-r--r--activejob/lib/active_job/serializers.rb64
-rw-r--r--activejob/lib/active_job/serializers/date_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/date_time_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/duration_serializer.rb24
-rw-r--r--activejob/lib/active_job/serializers/object_serializer.rb54
-rw-r--r--activejob/lib/active_job/serializers/symbol_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/time_serializer.rb21
-rw-r--r--activejob/lib/active_job/serializers/time_with_zone_serializer.rb21
-rw-r--r--activejob/lib/active_job/timezones.rb13
-rw-r--r--activejob/lib/active_job/translation.rb2
-rw-r--r--activejob/test/cases/argument_serialization_test.rb56
-rw-r--r--activejob/test/cases/job_serialization_test.rb7
-rw-r--r--activejob/test/cases/serializers_test.rb98
-rw-r--r--activejob/test/cases/timezones_test.rb24
-rw-r--r--activejob/test/integration/queuing_test.rb16
-rw-r--r--activejob/test/jobs/timezone_dependent_job.rb22
-rw-r--r--activejob/test/support/integration/dummy_app_template.rb1
-rw-r--r--activejob/test/support/integration/test_case_helpers.rb4
-rw-r--r--activemodel/CHANGELOG.md9
-rw-r--r--activemodel/activemodel.gemspec2
-rw-r--r--activemodel/lib/active_model/attribute.rb7
-rw-r--r--activemodel/lib/active_model/attribute/user_provided_default.rb3
-rw-r--r--activemodel/lib/active_model/attribute_mutation_tracker.rb7
-rw-r--r--activemodel/lib/active_model/attribute_set/builder.rb4
-rw-r--r--activemodel/lib/active_model/attribute_set/yaml_encoder.rb3
-rw-r--r--activemodel/lib/active_model/serialization.rb2
-rw-r--r--activemodel/lib/active_model/type/date.rb2
-rw-r--r--activemodel/lib/active_model/type/integer.rb7
-rw-r--r--activemodel/lib/active_model/type/registry.rb12
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb7
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb2
-rw-r--r--activemodel/test/cases/attribute_assignment_test.rb5
-rw-r--r--activemodel/test/cases/errors_test.rb3
-rw-r--r--activemodel/test/cases/serialization_test.rb7
-rw-r--r--activerecord/.gitignore3
-rw-r--r--activerecord/CHANGELOG.md32
-rw-r--r--activerecord/activerecord.gemspec2
-rw-r--r--activerecord/examples/.gitignore1
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/associations/association.rb4
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb10
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb7
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb2
-rw-r--r--activerecord/lib/active_record/base.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb3
-rw-r--r--activerecord/lib/active_record/enum.rb5
-rw-r--r--activerecord/lib/active_record/errors.rb4
-rw-r--r--activerecord/lib/active_record/fixtures.rb5
-rw-r--r--activerecord/lib/active_record/inheritance.rb46
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb2
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb1
-rw-r--r--activerecord/lib/active_record/migration.rb10
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb2
-rw-r--r--activerecord/lib/active_record/persistence.rb4
-rw-r--r--activerecord/lib/active_record/query_cache.rb12
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb65
-rw-r--r--activerecord/lib/active_record/railties/databases.rake2
-rw-r--r--activerecord/lib/active_record/reflection.rb17
-rw-r--r--activerecord/lib/active_record/relation.rb56
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb20
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb2
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb4
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb5
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/base_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb5
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/range_handler.rb10
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb17
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb1
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb3
-rw-r--r--activerecord/lib/active_record/statement_cache.rb3
-rw-r--r--activerecord/lib/active_record/table_metadata.rb5
-rw-r--r--activerecord/lib/active_record/test_databases.rb38
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb9
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb7
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb5
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb6
-rw-r--r--activerecord/test/.gitignore1
-rw-r--r--activerecord/test/cases/adapter_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb10
-rw-r--r--activerecord/test/cases/adapters/mysql2/transaction_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/date_test.rb42
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb39
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb20
-rw-r--r--activerecord/test/cases/adapters/postgresql/transaction_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb40
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb2
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb19
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb5
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb10
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb1
-rw-r--r--activerecord/test/cases/autosave_association_test.rb2
-rw-r--r--activerecord/test/cases/base_test.rb2
-rw-r--r--activerecord/test/cases/batches_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb18
-rw-r--r--activerecord/test/cases/core_test.rb81
-rw-r--r--activerecord/test/cases/dirty_test.rb18
-rw-r--r--activerecord/test/cases/explain_test.rb3
-rw-r--r--activerecord/test/cases/finder_test.rb14
-rw-r--r--activerecord/test/cases/fixtures_test.rb4
-rw-r--r--activerecord/test/cases/json_shared_test_cases.rb2
-rw-r--r--activerecord/test/cases/locking_test.rb4
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb2
-rw-r--r--activerecord/test/cases/migration_test.rb19
-rw-r--r--activerecord/test/cases/persistence_test.rb55
-rw-r--r--activerecord/test/cases/query_cache_test.rb15
-rw-r--r--activerecord/test/cases/relation/select_test.rb15
-rw-r--r--activerecord/test/cases/relations_test.rb35
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb2
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb2
-rw-r--r--activerecord/test/fixtures/.gitignore1
-rw-r--r--activerecord/test/models/author.rb3
-rw-r--r--activerecord/test/models/comment.rb4
-rw-r--r--activerecord/test/models/face.rb1
-rw-r--r--activerecord/test/models/man.rb3
-rw-r--r--activerecord/test/models/sponsor.rb1
-rw-r--r--activerecord/test/schema/schema.rb2
-rw-r--r--activestorage/.gitignore13
-rw-r--r--activestorage/CHANGELOG.md8
-rw-r--r--activestorage/activestorage.gemspec2
-rw-r--r--activestorage/app/assets/javascripts/activestorage.js2
-rw-r--r--activestorage/app/javascript/activestorage/blob_upload.js2
-rw-r--r--activestorage/app/models/active_storage/blob.rb16
-rw-r--r--activestorage/app/models/active_storage/blob/identifiable.rb7
-rw-r--r--activestorage/app/models/active_storage/filename.rb10
-rw-r--r--activestorage/app/models/active_storage/identification.rb15
-rw-r--r--activestorage/app/models/active_storage/preview.rb2
-rw-r--r--activestorage/app/models/active_storage/variant.rb2
-rw-r--r--activestorage/app/models/active_storage/variation.rb8
-rw-r--r--activestorage/lib/active_storage/engine.rb12
-rw-r--r--activestorage/lib/active_storage/service/gcs_service.rb9
-rw-r--r--activestorage/test/controllers/direct_uploads_controller_test.rb4
-rw-r--r--activestorage/test/fixtures/files/favicon.icobin0 -> 16958 bytes
-rw-r--r--activestorage/test/models/blob_test.rb19
-rw-r--r--activestorage/test/models/variant_test.rb11
-rw-r--r--activestorage/test/service/gcs_service_test.rb2
-rw-r--r--activestorage/test/service/s3_service_test.rb2
-rw-r--r--activesupport/.gitignore1
-rw-r--r--activesupport/CHANGELOG.md53
-rw-r--r--activesupport/activesupport.gemspec4
-rwxr-xr-xactivesupport/bin/generate_tables141
-rw-r--r--activesupport/lib/active_support/cache.rb17
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb13
-rw-r--r--activesupport/lib/active_support/cache/redis_cache_store.rb43
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb107
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb31
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb25
-rw-r--r--activesupport/lib/active_support/core_ext/module/redefine_method.rb25
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/conversions.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/regexp.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb41
-rw-r--r--activesupport/lib/active_support/core_ext/string/multibyte.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/string/strip.rb4
-rw-r--r--activesupport/lib/active_support/deprecation/behaviors.rb2
-rw-r--r--activesupport/lib/active_support/duration/iso8601_serializer.rb1
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb36
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb287
-rw-r--r--activesupport/lib/active_support/rails.rb6
-rw-r--r--activesupport/lib/active_support/subscriber.rb23
-rw-r--r--activesupport/lib/active_support/test_case.rb86
-rw-r--r--activesupport/lib/active_support/testing/parallelization.rb102
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb3
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb8
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin1116857 -> 0 bytes
-rw-r--r--activesupport/lib/active_support/xml_mini.rb6
-rw-r--r--activesupport/test/cache/behaviors/connection_pool_behavior.rb7
-rw-r--r--activesupport/test/cache/behaviors/local_cache_behavior.rb10
-rw-r--r--activesupport/test/cache/stores/redis_cache_store_test.rb45
-rw-r--r--activesupport/test/callbacks_test.rb3
-rw-r--r--activesupport/test/core_ext/array/grouping_test.rb9
-rw-r--r--activesupport/test/core_ext/duration_test.rb3
-rw-r--r--activesupport/test/core_ext/hash/transform_values_test.rb69
-rw-r--r--activesupport/test/core_ext/module_test.rb57
-rw-r--r--activesupport/test/core_ext/object/duplicable_test.rb8
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb68
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb9
-rw-r--r--activesupport/test/hash_with_indifferent_access_test.rb16
-rw-r--r--activesupport/test/multibyte_unicode_database_test.rb26
-rw-r--r--activesupport/test/time_zone_test.rb10
-rw-r--r--guides/CHANGELOG.md5
-rw-r--r--guides/bug_report_templates/action_controller_gem.rb2
-rw-r--r--guides/bug_report_templates/active_job_gem.rb2
-rw-r--r--guides/bug_report_templates/active_record_gem.rb2
-rw-r--r--guides/bug_report_templates/active_record_migrations_gem.rb4
-rw-r--r--guides/bug_report_templates/active_record_migrations_master.rb2
-rw-r--r--guides/bug_report_templates/generic_gem.rb3
-rw-r--r--guides/rails_guides/kindle.rb4
-rw-r--r--guides/source/active_job_basics.md54
-rw-r--r--guides/source/active_record_callbacks.md2
-rw-r--r--guides/source/active_storage_overview.md26
-rw-r--r--guides/source/active_support_core_extensions.md91
-rw-r--r--guides/source/autoloading_and_reloading_constants.md2
-rw-r--r--guides/source/caching_with_rails.md11
-rw-r--r--guides/source/command_line.md10
-rw-r--r--guides/source/configuring.md39
-rw-r--r--guides/source/getting_started.md30
-rw-r--r--guides/source/initialization.md2
-rw-r--r--guides/source/routing.md2
-rw-r--r--guides/source/security.md12
-rw-r--r--guides/source/testing.md83
-rw-r--r--guides/source/upgrading_ruby_on_rails.md1
-rw-r--r--rails.gemspec2
-rw-r--r--railties/.gitignore6
-rw-r--r--railties/CHANGELOG.md5
-rw-r--r--railties/lib/minitest/rails_plugin.rb11
-rw-r--r--railties/lib/rails/application.rb3
-rw-r--r--railties/lib/rails/application/configuration.rb87
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb3
-rw-r--r--railties/lib/rails/command.rb1
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb2
-rw-r--r--railties/lib/rails/commands/encrypted/encrypted_command.rb3
-rw-r--r--railties/lib/rails/commands/routes/routes_command.rb43
-rw-r--r--railties/lib/rails/generators/actions.rb2
-rw-r--r--railties/lib/rails/generators/app_base.rb1
-rw-r--r--railties/lib/rails/generators/named_base.rb6
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb20
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt30
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb1
-rw-r--r--railties/lib/rails/generators/rails/credentials/credentials_generator.rb11
-rw-r--r--railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb7
-rw-r--r--railties/lib/rails/generators/rails/master_key/master_key_generator.rb4
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt1
-rw-r--r--railties/lib/rails/generators/resource_helpers.rb7
-rw-r--r--railties/lib/rails/ruby_version_check.rb6
-rw-r--r--railties/lib/rails/secrets.rb1
-rw-r--r--railties/lib/rails/tasks.rb1
-rw-r--r--railties/lib/rails/tasks/routes.rake31
-rw-r--r--railties/lib/rails/test_help.rb1
-rw-r--r--railties/railties.gemspec2
-rw-r--r--railties/test/application/configuration_test.rb48
-rw-r--r--railties/test/application/content_security_policy_test.rb40
-rw-r--r--railties/test/application/loading_test.rb12
-rw-r--r--railties/test/application/middleware_test.rb3
-rw-r--r--railties/test/application/rake_test.rb114
-rw-r--r--railties/test/application/test_runner_test.rb84
-rw-r--r--railties/test/application/test_test.rb5
-rw-r--r--railties/test/commands/credentials_test.rb12
-rw-r--r--railties/test/commands/encrypted_test.rb12
-rw-r--r--railties/test/commands/routes_test.rb127
-rw-r--r--railties/test/generators/api_app_generator_test.rb19
-rw-r--r--railties/test/generators/app_generator_test.rb41
-rw-r--r--railties/test/generators/controller_generator_test.rb7
-rw-r--r--railties/test/generators/model_generator_test.rb35
-rw-r--r--railties/test/generators/plugin_generator_test.rb18
-rw-r--r--railties/test/generators/shared_generator_tests.rb2
-rw-r--r--railties/test/isolation/abstract_unit.rb7
-rw-r--r--railties/test/minitest/rails_plugin_test.rb42
354 files changed, 3603 insertions, 1831 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index d59a0780d1..e4568d9d8b 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -23,7 +23,7 @@ checks:
engines:
rubocop:
enabled: true
- channel: rubocop-0-51
+ channel: rubocop-0-52
ratings:
paths:
diff --git a/.gitignore b/.gitignore
index da3b42cfbb..ab9ca8a6ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,25 +1,16 @@
-# Don't put *.swp, *.bak, etc here; those belong in a global ~/.gitignore.
+# Don't put *.swp, *.bak, etc here; those belong in a global .gitignore.
# Check out https://help.github.com/articles/ignoring-files for how to set that up.
.Gemfile
-.ruby-version
.byebug_history
-debug.log
-pkg
+.ruby-version
+/*/doc/
+/*/test/tmp/
/.bundle
-/dist
-/doc/rdoc
-/*/doc
-/*/test/tmp
-/activerecord/sqlnet.log
-/activemodel/test/fixtures/fixture_database.sqlite3
-/activesupport/test/fixtures/isolation_test
-/activestorage/test/service/configurations.yml
-/railties/test/500.html
-/railties/test/fixtures/tmp
-/railties/test/initializer/root/log
-/railties/doc
-/railties/tmp
-/guides/output
+/dist/
+/doc/
+/guides/output/
+debug.log
node_modules/
-/actionview/log
+package-lock.json
+pkg/
diff --git a/.rubocop.yml b/.rubocop.yml
index 3b4dd79e81..3c765d5b1d 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,5 +1,5 @@
AllCops:
- TargetRubyVersion: 2.2
+ TargetRubyVersion: 2.4
# RuboCop has a bunch of cops enabled by default. This setting tells RuboCop
# to ignore them, so only the ones explicitly set in this file are enabled.
DisabledByDefault: true
@@ -156,3 +156,7 @@ Style/RedundantReturn:
Style/Semicolon:
Enabled: true
AllowAsExpressionSeparator: true
+
+# Prefer Foo.method over Foo::method
+Style/ColonMethodCall:
+ Enabled: true
diff --git a/.travis.yml b/.travis.yml
index 3a7f28134b..2513e87114 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -60,8 +60,6 @@ env:
- "GEM=ac:integration"
rvm:
- - 2.2.9
- - 2.3.6
- 2.4.3
- 2.5.0
- ruby-head
@@ -70,18 +68,6 @@ matrix:
include:
- rvm: 2.5.0
env: "GEM=av:ujs"
- - rvm: 2.2.9
- env: "GEM=aj:integration"
- services:
- - memcached
- - redis-server
- - rabbitmq
- - rvm: 2.3.6
- env: "GEM=aj:integration"
- services:
- - memcached
- - redis-server
- - rabbitmq
- rvm: 2.4.3
env: "GEM=aj:integration"
services:
@@ -100,30 +86,30 @@ matrix:
- memcached
- redis-server
- rabbitmq
- - rvm: 2.3.6
+ - rvm: 2.5.0
env:
- "GEM=ar:mysql2 MYSQL=mariadb"
addons:
mariadb: 10.2
- - rvm: 2.3.6
+ - rvm: 2.5.0
env:
- "GEM=ar:sqlite3_mem"
- - rvm: 2.3.6
+ - rvm: 2.5.0
env:
- "GEM=ar:postgresql POSTGRES=9.2"
addons:
postgresql: "9.2"
- - rvm: jruby-9.1.15.0
+ - rvm: jruby-head
jdk: oraclejdk8
env:
- "GEM=ap"
- - rvm: jruby-9.1.15.0
+ - rvm: jruby-head
jdk: oraclejdk8
env:
- "GEM=am,amo,aj"
allow_failures:
- rvm: ruby-head
- - rvm: jruby-9.1.15.0
+ - rvm: jruby-head
- env: "GEM=ac:integration"
fast_finish: true
diff --git a/Gemfile b/Gemfile
index 8c5f31ef99..aada2bde1e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -38,9 +38,6 @@ gem "rubocop", ">= 0.47", require: false
# https://github.com/guard/rb-inotify/pull/79
gem "rb-inotify", github: "matthewd/rb-inotify", branch: "close-handling", require: false
-# https://github.com/puma/puma/pull/1345
-gem "stopgap_13632", platforms: :mri if RUBY_VERSION == "2.2.8"
-
group :doc do
gem "sdoc", "~> 1.0"
gem "redcarpet", "~> 3.2.3", platforms: :ruby
@@ -52,7 +49,7 @@ end
gem "dalli", ">= 2.2.1"
gem "listen", ">= 3.0.5", "< 3.2", require: false
gem "libxml-ruby", platforms: :ruby
-gem "connection_pool"
+gem "connection_pool", require: false
# for railties app_generator_test
gem "bootsnap", ">= 1.1.0", require: false
@@ -131,7 +128,7 @@ platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
group :db do
gem "pg", ">= 0.18.0"
- gem "mysql2", ">= 0.4.4"
+ gem "mysql2", ">= 0.4.10"
end
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 0c9b58febc..baac1cee7e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -64,7 +64,7 @@ PATH
marcel (~> 0.3.1)
activesupport (6.0.0.alpha)
concurrent-ruby (~> 1.0, >= 1.0.2)
- i18n (~> 0.7)
+ i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
rails (6.0.0.alpha)
@@ -107,7 +107,7 @@ GEM
archive-zip (0.7.0)
io-like (~> 0.3.0)
arel (9.0.0)
- ast (2.3.0)
+ ast (2.4.0)
aws-partitions (1.20.0)
aws-sdk-core (3.3.0)
aws-partitions (~> 1.0)
@@ -266,7 +266,7 @@ GEM
hiredis (0.6.1-java)
http_parser.rb (0.6.0)
httpclient (2.8.3)
- i18n (0.9.3)
+ i18n (1.0.0)
concurrent-ruby (~> 1.0)
io-like (0.3.0)
jdbc-mysql (5.1.44)
@@ -321,10 +321,10 @@ GEM
multi_json (1.12.2)
multipart-post (2.0.0)
mustache (1.0.5)
- mustermann (1.0.0)
- mysql2 (0.4.9)
- mysql2 (0.4.9-x64-mingw32)
- mysql2 (0.4.9-x86-mingw32)
+ mustermann (1.0.2)
+ mysql2 (0.4.10)
+ mysql2 (0.4.10-x64-mingw32)
+ mysql2 (0.4.10-x86-mingw32)
nio4r (2.2.0)
nio4r (2.2.0-java)
nokogiri (1.8.1)
@@ -335,9 +335,9 @@ GEM
nokogiri (1.8.1-x86-mingw32)
mini_portile2 (~> 2.3.0)
os (0.9.6)
- parallel (1.12.0)
- parser (2.4.0.0)
- ast (~> 2.2)
+ parallel (1.12.1)
+ parser (2.5.0.2)
+ ast (~> 2.4.0)
path_expander (1.0.2)
pg (1.0.0)
pg (1.0.0-x64-mingw32)
@@ -352,10 +352,10 @@ GEM
selenium-webdriver
thor
racc (1.4.14)
- rack (2.0.3)
+ rack (2.0.4)
rack-cache (1.7.0)
rack (>= 0.4)
- rack-protection (2.0.0)
+ rack-protection (2.0.1)
rack
rack-test (0.8.0)
rack (>= 1.0, < 3)
@@ -364,9 +364,8 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- rainbow (2.2.2)
- rake
- rake (12.2.1)
+ rainbow (3.0.0)
+ rake (12.3.0)
rb-fsevent (0.10.2)
rdoc (6.0.1)
redcarpet (3.2.3)
@@ -389,11 +388,11 @@ GEM
resque (~> 1.26)
rufus-scheduler (~> 3.2)
retriable (3.1.1)
- rubocop (0.51.0)
+ rubocop (0.52.1)
parallel (~> 1.10)
- parser (>= 2.3.3.1, < 3.0)
+ parser (>= 2.4.0.2, < 3.0)
powerpack (~> 0.1)
- rainbow (>= 2.2.2, < 3.0)
+ rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
ruby-progressbar (1.9.0)
@@ -432,10 +431,10 @@ GEM
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
- sinatra (2.0.0)
+ sinatra (2.0.1)
mustermann (~> 1.0)
rack (~> 2.0)
- rack-protection (= 2.0.0)
+ rack-protection (= 2.0.1)
tilt (~> 2.0)
sneakers (2.5.0)
bunny (~> 2.6.4)
@@ -531,7 +530,7 @@ DEPENDENCIES
mini_magick
minitest-bisect
mocha
- mysql2 (>= 0.4.4)
+ mysql2 (>= 0.4.10)
nokogiri (>= 1.8.1)
pg (>= 0.18.0)
psych (~> 2.0)
diff --git a/actioncable/.gitignore b/actioncable/.gitignore
index 0a04b29786..f514e58c16 100644
--- a/actioncable/.gitignore
+++ b/actioncable/.gitignore
@@ -1,2 +1,2 @@
-/lib/assets/compiled
-/tmp
+/lib/assets/compiled/
+/tmp/
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index 72f5c5de35..a28e2fe75d 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,3 +1,8 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actioncable/CHANGELOG.md) for previous changes.
diff --git a/actioncable/actioncable.gemspec b/actioncable/actioncable.gemspec
index 51db4dda3a..d946d0797f 100644
--- a/actioncable/actioncable.gemspec
+++ b/actioncable/actioncable.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "WebSocket framework for Rails."
s.description = "Structure many real-time application concerns into channels over a single WebSocket connection."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index 84053db9fd..11a1f1a5e8 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -136,13 +136,10 @@ module ActionCable
send_async :handle_close
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :websocket
attr_reader :message_buffer
- private
# The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
def request # :doc:
@request ||= begin
diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb
index f151a47072..965841b67e 100644
--- a/actioncable/lib/action_cable/connection/message_buffer.rb
+++ b/actioncable/lib/action_cable/connection/message_buffer.rb
@@ -30,13 +30,10 @@ module ActionCable
receive_buffered_messages
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :connection
attr_reader :buffered_messages
- private
def valid?(message)
message.is_a?(String)
end
diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb
index bb8d64e27a..1ad8d05107 100644
--- a/actioncable/lib/action_cable/connection/subscriptions.rb
+++ b/actioncable/lib/action_cable/connection/subscriptions.rb
@@ -63,12 +63,8 @@ module ActionCable
subscriptions.each { |id, channel| remove_subscription(channel) }
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :connection, :subscriptions
-
private
+ attr_reader :connection, :subscriptions
delegate :logger, to: :connection
def find(data)
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 81233ace34..31f29fdd2f 100644
--- a/actioncable/lib/action_cable/connection/web_socket.rb
+++ b/actioncable/lib/action_cable/connection/web_socket.rb
@@ -34,9 +34,7 @@ module ActionCable
websocket.rack_response
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :websocket
end
end
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index b2a1c570e5..c5dbb0f1d4 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,3 +1,8 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionmailer/CHANGELOG.md) for previous changes.
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index b8a2e80bd3..f2fb160bdd 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Email composition, delivery, and receiving framework (part of Rails)."
s.description = "Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 0d10a9a3f2..cd419b68f7 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,9 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
* Add alias method `to_hash` to `to_h` for `cookies`.
Add alias method `to_h` to `to_hash` for `session`.
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index 33d42e69d8..1dc8abf746 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Web-flow and rendering framework putting the VC in MVC (part of Rails)."
s.description = "Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb
index 48a7109bea..95f2f3242d 100644
--- a/actionpack/lib/action_controller/metal/content_security_policy.rb
+++ b/actionpack/lib/action_controller/metal/content_security_policy.rb
@@ -5,6 +5,14 @@ module ActionController #:nodoc:
# TODO: Documentation
extend ActiveSupport::Concern
+ include AbstractController::Helpers
+ include AbstractController::Callbacks
+
+ included do
+ helper_method :content_security_policy?
+ helper_method :content_security_policy_nonce
+ end
+
module ClassMethods
def content_security_policy(**options, &block)
before_action(options) do
@@ -22,5 +30,15 @@ module ActionController #:nodoc:
end
end
end
+
+ private
+
+ def content_security_policy?
+ request.content_security_policy
+ end
+
+ def content_security_policy_nonce
+ request.content_security_policy_nonce
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 0ab313e398..94092de96c 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -3,7 +3,6 @@
require "rack/session/abstract/id"
require "action_controller/metal/exceptions"
require "active_support/security_utils"
-require "active_support/core_ext/string/strip"
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
@@ -416,7 +415,7 @@ module ActionController #:nodoc:
allow_forgery_protection
end
- NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc
+ NULL_ORIGIN_MESSAGE = <<~MSG
The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually
means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that
refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index a56ac749f8..615c90c496 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/hash/indifferent_access"
-require "active_support/core_ext/hash/transform_values"
require "active_support/core_ext/array/wrap"
require "active_support/core_ext/string/filters"
require "active_support/core_ext/object/to_query"
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
index 4883e23d24..a3407c9698 100644
--- a/actionpack/lib/action_dispatch/http/content_security_policy.rb
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -21,6 +21,12 @@ module ActionDispatch #:nodoc:
return response if policy_present?(headers)
if policy = request.content_security_policy
+ if policy.directives["script-src"]
+ if nonce = request.content_security_policy_nonce
+ policy.directives["script-src"] << "'nonce-#{nonce}'"
+ end
+ end
+
headers[header_name(request)] = policy.build(request.controller_instance)
end
@@ -51,6 +57,8 @@ module ActionDispatch #:nodoc:
module Request
POLICY = "action_dispatch.content_security_policy".freeze
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze
+ NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator".freeze
+ NONCE = "action_dispatch.content_security_policy_nonce".freeze
def content_security_policy
get_header(POLICY)
@@ -67,6 +75,30 @@ module ActionDispatch #:nodoc:
def content_security_policy_report_only=(value)
set_header(POLICY_REPORT_ONLY, value)
end
+
+ def content_security_policy_nonce_generator
+ get_header(NONCE_GENERATOR)
+ end
+
+ def content_security_policy_nonce_generator=(generator)
+ set_header(NONCE_GENERATOR, generator)
+ end
+
+ def content_security_policy_nonce
+ if content_security_policy_nonce_generator
+ if nonce = get_header(NONCE)
+ nonce
+ else
+ set_header(NONCE, generate_content_security_policy_nonce)
+ end
+ end
+ end
+
+ private
+
+ def generate_content_security_policy_nonce
+ content_security_policy_nonce_generator.call(self)
+ end
end
MAPPINGS = {
@@ -172,7 +204,7 @@ module ActionDispatch #:nodoc:
end
def build(context = nil)
- build_directives(context).compact.join("; ") + ";"
+ build_directives(context).compact.join("; ")
end
private
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index d2b2106845..295539281f 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -279,8 +279,6 @@ module Mime
def all?; false; end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
attr_reader :string, :synonyms
diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
index 08b931a3cd..32f632800c 100644
--- a/actionpack/lib/action_dispatch/journey/nodes/node.rb
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -32,7 +32,7 @@ module ActionDispatch
end
def name
- left.tr "*:".freeze, "".freeze
+ -left.tr("*:", "")
end
def type
@@ -82,7 +82,7 @@ module ActionDispatch
def initialize(left)
super
@regexp = DEFAULT_EXP
- @name = left.tr "*:".freeze, "".freeze
+ @name = -left.tr("*:", "")
end
def default_regexp?
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 2d85a89a56..537f479ee5 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -90,7 +90,7 @@ module ActionDispatch
return @separator_re unless @matchers.key?(node)
re = @matchers[node]
- "(#{re})"
+ "(#{Regexp.union(re)})"
end
def visit_GROUP(node)
@@ -183,7 +183,7 @@ module ActionDispatch
node = node.to_sym
if @requirements.key?(node)
- re = /#{@requirements[node]}|/
+ re = /#{Regexp.union(@requirements[node])}|/
@offsets.push((re.match("").length - 1) + @offsets.last)
else
@offsets << @offsets.last
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
index 4ae77903fa..2a075862e9 100644
--- a/actionpack/lib/action_dispatch/journey/scanner.rb
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -34,6 +34,13 @@ module ActionDispatch
private
+ # takes advantage of String @- deduping capabilities in Ruby 2.5 upwards
+ # see: https://bugs.ruby-lang.org/issues/13077
+ def dedup_scan(regex)
+ r = @ss.scan(regex)
+ r ? -r : nil
+ end
+
def scan
case
# /
@@ -47,15 +54,15 @@ module ActionDispatch
[:OR, "|"]
when @ss.skip(/\./)
[:DOT, "."]
- when text = @ss.scan(/:\w+/)
+ when text = dedup_scan(/:\w+/)
[:SYMBOL, text]
- when text = @ss.scan(/\*\w+/)
+ when text = dedup_scan(/\*\w+/)
[:STAR, text]
when text = @ss.scan(/(?:[\w%\-~!$&'*+,;=@]|\\[:()])+/)
text.tr! "\\", ""
- [:LITERAL, text]
+ [:LITERAL, -text]
# any char
- when text = @ss.scan(/./)
+ when text = dedup_scan(/./)
[:LITERAL, text]
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index ef633aadc6..6d9f36ad75 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -26,8 +26,8 @@ module ActionDispatch
# Set +config.ssl_options+ with <tt>hsts: { ... }</tt> to configure HSTS:
#
# * +expires+: How long, in seconds, these settings will stick. The minimum
- # required to qualify for browser preload lists is 18 weeks. Defaults to
- # 180 days (recommended).
+ # required to qualify for browser preload lists is 1 year. Defaults to
+ # 1 year (recommended).
#
# * +subdomains+: Set to +true+ to tell the browser to apply these settings
# to all subdomains. This protects your cookies from interception by a
@@ -47,9 +47,8 @@ module ActionDispatch
class SSL
# :stopdoc:
- # Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
- # and greater than the 18-week requirement for browser preload lists.
- HSTS_EXPIRES_IN = 15552000
+ # Default to 1 year, the minimum for browser preload lists.
+ HSTS_EXPIRES_IN = 31536000
def self.default_hsts_options
{ expires: HSTS_EXPIRES_IN, subdomains: true, preload: false }
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index a2205569b4..22336c59b6 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "delegate"
-require "active_support/core_ext/string/strip"
module ActionDispatch
module Routing
@@ -150,10 +149,10 @@ module ActionDispatch
def no_routes(routes)
@buffer <<
if routes.none?
- <<-MESSAGE.strip_heredoc
- You don't have any routes defined!
+ <<~MESSAGE
+ You don't have any routes defined!
- Please add some routes in config/routes.rb.
+ Please add some routes in config/routes.rb.
MESSAGE
else
"No routes were found for this controller"
@@ -203,7 +202,7 @@ module ActionDispatch
end
def no_routes(*)
- @buffer << <<-MESSAGE.strip_heredoc
+ @buffer << <<~MESSAGE
<p>You don't have any routes defined!</p>
<ul>
<li>Please add some routes in <tt>config/routes.rb</tt>.</li>
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 31eb6104fe..f3970d5445 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -611,7 +611,7 @@ module ActionDispatch
end
raise ArgumentError, "A rack application must be specified" unless app.respond_to?(:call)
- raise ArgumentError, <<-MSG.strip_heredoc unless path
+ raise ArgumentError, <<~MSG unless path
Must be called with mount point
mount SomeRackApp, at: "some_route"
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 9eff30fa53..a29a5a04ef 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -2,7 +2,6 @@
require "action_dispatch/journey"
require "active_support/core_ext/object/to_query"
-require "active_support/core_ext/hash/slice"
require "active_support/core_ext/module/redefine_method"
require "active_support/core_ext/module/remove_method"
require "active_support/core_ext/array/extract_options"
@@ -154,13 +153,13 @@ module ActionDispatch
url_name = :"#{name}_url"
@path_helpers_module.module_eval do
- define_method(path_name) do |*args|
+ redefine_method(path_name) do |*args|
helper.call(self, args, true)
end
end
@url_helpers_module.module_eval do
- define_method(url_name) do |*args|
+ redefine_method(url_name) do |*args|
helper.call(self, args, false)
end
end
@@ -855,7 +854,7 @@ module ActionDispatch
recognize_path_with_request(req, path, extras)
end
- def recognize_path_with_request(req, path, extras)
+ def recognize_path_with_request(req, path, extras, raise_on_missing: true)
@router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
@@ -875,12 +874,14 @@ module ActionDispatch
return req.path_parameters
elsif app.matches?(req) && app.engine?
- path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras)
- return path_parameters
+ path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false)
+ return path_parameters if path_parameters
end
end
- raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ if raise_on_missing
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
end
end
# :startdoc:
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
index df0c5d3f0e..2c8cee3a9b 100644
--- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb
@@ -43,7 +43,7 @@ module ActionDispatch
end
def image_path
- @image_path ||= absolute_image_path.relative_path_from(Pathname.pwd).to_s
+ @image_path ||= absolute_image_path.to_s
end
def absolute_image_path
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
index 76ff784926..560157dc61 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -9,7 +9,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
before_action :authenticate_with_request, only: :display
USERS = { "lifo" => "world", "pretty" => "please",
- "dhh" => ::Digest::MD5::hexdigest(["dhh", "SuperSecret", "secret"].join(":")) }
+ "dhh" => ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")) }
def index
render plain: "Hello Secret"
@@ -181,9 +181,10 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
end
test "authentication request with password stored as ha1 digest hash" do
- @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: "dhh",
- password: ::Digest::MD5::hexdigest(["dhh", "SuperSecret", "secret"].join(":")),
- password_is_ha1: true)
+ @request.env["HTTP_AUTHORIZATION"] = encode_credentials(
+ username: "dhh",
+ password: ::Digest::MD5.hexdigest(["dhh", "SuperSecret", "secret"].join(":")),
+ password_is_ha1: true)
get :display
assert_response :success
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
index 7bcf8c380d..07a897a103 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -2,7 +2,6 @@
require "abstract_unit"
require "action_controller/metal/strong_parameters"
-require "active_support/core_ext/hash/transform_values"
class ParametersAccessorsTest < ActiveSupport::TestCase
setup do
@@ -273,23 +272,16 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
assert_match(/permitted: true/, @params.inspect)
end
- if Hash.method_defined?(:dig)
- test "#dig delegates the dig method to its values" do
- assert_equal "David", @params.dig(:person, :name, :first)
- assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
- end
+ test "#dig delegates the dig method to its values" do
+ assert_equal "David", @params.dig(:person, :name, :first)
+ assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
+ end
- test "#dig converts hashes to parameters" do
- assert_kind_of ActionController::Parameters, @params.dig(:person)
- assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
- assert @params.dig(:person, :addresses).all? do |value|
- value.is_a?(ActionController::Parameters)
- end
- end
- else
- test "ActionController::Parameters does not respond to #dig on Ruby 2.2" do
- assert_not ActionController::Parameters.method_defined?(:dig)
- assert_not_respond_to @params, :dig
+ test "#dig converts hashes to parameters" do
+ assert_kind_of ActionController::Parameters, @params.dig(:person)
+ assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
+ assert @params.dig(:person, :addresses).all? do |value|
+ value.is_a?(ActionController::Parameters)
end
end
end
diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb
index 4d9fea7188..312b1e5b27 100644
--- a/actionpack/test/controller/parameters/mutators_test.rb
+++ b/actionpack/test/controller/parameters/mutators_test.rb
@@ -2,7 +2,6 @@
require "abstract_unit"
require "action_controller/metal/strong_parameters"
-require "active_support/core_ext/hash/transform_values"
class ParametersMutatorsTest < ActiveSupport::TestCase
setup do
diff --git a/actionpack/test/controller/parameters/serialization_test.rb b/actionpack/test/controller/parameters/serialization_test.rb
index 7809d0f357..7708c8e4fe 100644
--- a/actionpack/test/controller/parameters/serialization_test.rb
+++ b/actionpack/test/controller/parameters/serialization_test.rb
@@ -2,7 +2,6 @@
require "abstract_unit"
require "action_controller/metal/strong_parameters"
-require "active_support/core_ext/string/strip"
class ParametersSerializationTest < ActiveSupport::TestCase
setup do
@@ -31,7 +30,7 @@ class ParametersSerializationTest < ActiveSupport::TestCase
end
test "yaml backwardscompatible with psych 2.0.8 format" do
- params = YAML.load <<-end_of_yaml.strip_heredoc
+ params = YAML.load <<~end_of_yaml
--- !ruby/hash:ActionController::Parameters
key: :value
end_of_yaml
@@ -41,7 +40,7 @@ class ParametersSerializationTest < ActiveSupport::TestCase
end
test "yaml backwardscompatible with psych 2.0.9+ format" do
- params = YAML.load(<<-end_of_yaml.strip_heredoc)
+ params = YAML.load(<<~end_of_yaml)
--- !ruby/hash-with-ivars:ActionController::Parameters
elements:
key: :value
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index ec939e946a..9c0e101f7c 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -676,7 +676,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
token = "\321\202\320\265\320\272\321\201\321\202".dup # 'text' in Russian
token.force_encoding(Encoding::BINARY)
- escaped_token = CGI::escape(token)
+ escaped_token = CGI.escape(token)
assert_equal "/page/" + escaped_token, url_for(rs, controller: "content", action: "show_page", id: token)
assert_equal({ controller: "content", action: "show_page", id: token }, rs.recognize_path("/page/#{escaped_token}"))
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
index 7c4a65a633..b88f90190a 100644
--- a/actionpack/test/dispatch/content_security_policy_test.rb
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -8,10 +8,10 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
end
def test_build
- assert_equal ";", @policy.build
+ assert_equal "", @policy.build
@policy.script_src :self
- assert_equal "script-src 'self';", @policy.build
+ assert_equal "script-src 'self'", @policy.build
end
def test_dup
@@ -25,34 +25,34 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
def test_mappings
@policy.script_src :data
- assert_equal "script-src data:;", @policy.build
+ assert_equal "script-src data:", @policy.build
@policy.script_src :mediastream
- assert_equal "script-src mediastream:;", @policy.build
+ assert_equal "script-src mediastream:", @policy.build
@policy.script_src :blob
- assert_equal "script-src blob:;", @policy.build
+ assert_equal "script-src blob:", @policy.build
@policy.script_src :filesystem
- assert_equal "script-src filesystem:;", @policy.build
+ assert_equal "script-src filesystem:", @policy.build
@policy.script_src :self
- assert_equal "script-src 'self';", @policy.build
+ assert_equal "script-src 'self'", @policy.build
@policy.script_src :unsafe_inline
- assert_equal "script-src 'unsafe-inline';", @policy.build
+ assert_equal "script-src 'unsafe-inline'", @policy.build
@policy.script_src :unsafe_eval
- assert_equal "script-src 'unsafe-eval';", @policy.build
+ assert_equal "script-src 'unsafe-eval'", @policy.build
@policy.script_src :none
- assert_equal "script-src 'none';", @policy.build
+ assert_equal "script-src 'none'", @policy.build
@policy.script_src :strict_dynamic
- assert_equal "script-src 'strict-dynamic';", @policy.build
+ assert_equal "script-src 'strict-dynamic'", @policy.build
@policy.script_src :none, :report_sample
- assert_equal "script-src 'none' 'report-sample';", @policy.build
+ assert_equal "script-src 'none' 'report-sample'", @policy.build
end
def test_fetch_directives
@@ -131,16 +131,16 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
def test_document_directives
@policy.base_uri "https://example.com"
- assert_match %r{base-uri https://example\.com;}, @policy.build
+ assert_match %r{base-uri https://example\.com}, @policy.build
@policy.plugin_types "application/x-shockwave-flash"
- assert_match %r{plugin-types application/x-shockwave-flash;}, @policy.build
+ assert_match %r{plugin-types application/x-shockwave-flash}, @policy.build
@policy.sandbox
- assert_match %r{sandbox;}, @policy.build
+ assert_match %r{sandbox}, @policy.build
@policy.sandbox "allow-scripts", "allow-modals"
- assert_match %r{sandbox allow-scripts allow-modals;}, @policy.build
+ assert_match %r{sandbox allow-scripts allow-modals}, @policy.build
@policy.sandbox false
assert_no_match %r{sandbox}, @policy.build
@@ -148,35 +148,35 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
def test_navigation_directives
@policy.form_action :self
- assert_match %r{form-action 'self';}, @policy.build
+ assert_match %r{form-action 'self'}, @policy.build
@policy.frame_ancestors :self
- assert_match %r{frame-ancestors 'self';}, @policy.build
+ assert_match %r{frame-ancestors 'self'}, @policy.build
end
def test_reporting_directives
@policy.report_uri "/violations"
- assert_match %r{report-uri /violations;}, @policy.build
+ assert_match %r{report-uri /violations}, @policy.build
end
def test_other_directives
@policy.block_all_mixed_content
- assert_match %r{block-all-mixed-content;}, @policy.build
+ assert_match %r{block-all-mixed-content}, @policy.build
@policy.block_all_mixed_content false
assert_no_match %r{block-all-mixed-content}, @policy.build
@policy.require_sri_for :script, :style
- assert_match %r{require-sri-for script style;}, @policy.build
+ assert_match %r{require-sri-for script style}, @policy.build
@policy.require_sri_for "script", "style"
- assert_match %r{require-sri-for script style;}, @policy.build
+ assert_match %r{require-sri-for script style}, @policy.build
@policy.require_sri_for
assert_no_match %r{require-sri-for}, @policy.build
@policy.upgrade_insecure_requests
- assert_match %r{upgrade-insecure-requests;}, @policy.build
+ assert_match %r{upgrade-insecure-requests}, @policy.build
@policy.upgrade_insecure_requests false
assert_no_match %r{upgrade-insecure-requests}, @policy.build
@@ -184,13 +184,13 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
def test_multiple_sources
@policy.script_src :self, :https
- assert_equal "script-src 'self' https:;", @policy.build
+ assert_equal "script-src 'self' https:", @policy.build
end
def test_multiple_directives
@policy.script_src :self, :https
@policy.style_src :self, :https
- assert_equal "script-src 'self' https:; style-src 'self' https:;", @policy.build
+ assert_equal "script-src 'self' https:; style-src 'self' https:", @policy.build
end
def test_dynamic_directives
@@ -198,12 +198,12 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
controller = Struct.new(:request).new(request)
@policy.script_src -> { request.host }
- assert_equal "script-src www.example.com;", @policy.build(controller)
+ assert_equal "script-src www.example.com", @policy.build(controller)
end
def test_mixed_static_and_dynamic_directives
@policy.script_src :self, -> { "foo.com" }, "bar.com"
- assert_equal "script-src 'self' foo.com bar.com;", @policy.build(Object.new)
+ assert_equal "script-src 'self' foo.com bar.com", @policy.build(Object.new)
end
def test_invalid_directive_source
@@ -253,6 +253,11 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
p.report_uri "/violations"
end
+ content_security_policy only: :script_src do |p|
+ p.default_src false
+ p.script_src :self
+ end
+
content_security_policy_report_only only: :report_only
def index
@@ -271,6 +276,10 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
head :ok
end
+ def script_src
+ head :ok
+ end
+
private
def condition?
params[:condition] == "true"
@@ -284,6 +293,7 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
get "/inline", to: "policy#inline"
get "/conditional", to: "policy#conditional"
get "/report-only", to: "policy#report_only"
+ get "/script-src", to: "policy#script_src"
end
end
@@ -298,6 +308,7 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
def call(env)
env["action_dispatch.content_security_policy"] = POLICY
+ env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" }
env["action_dispatch.content_security_policy_report_only"] = false
env["action_dispatch.show_exceptions"] = false
@@ -316,25 +327,30 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
def test_generates_content_security_policy_header
get "/"
- assert_policy "default-src 'self';"
+ assert_policy "default-src 'self'"
end
def test_generates_inline_content_security_policy
get "/inline"
- assert_policy "default-src https://example.com;"
+ assert_policy "default-src https://example.com"
end
def test_generates_conditional_content_security_policy
get "/conditional", params: { condition: "true" }
- assert_policy "default-src https://true.example.com;"
+ assert_policy "default-src https://true.example.com"
get "/conditional", params: { condition: "false" }
- assert_policy "default-src https://false.example.com;"
+ assert_policy "default-src https://false.example.com"
end
def test_generates_report_only_content_security_policy
get "/report-only"
- assert_policy "default-src 'self'; report-uri /violations;", report_only: true
+ assert_policy "default-src 'self'; report-uri /violations", report_only: true
+ end
+
+ def test_adds_nonce_to_script_src_content_security_policy
+ get "/script-src"
+ assert_policy "script-src 'self' 'nonce-iyhD0Yc0W+c='"
end
private
diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb
index a5198f2f13..009b6d9bc3 100644
--- a/actionpack/test/dispatch/routing_assertions_test.rb
+++ b/actionpack/test/dispatch/routing_assertions_test.rb
@@ -52,6 +52,8 @@ class RoutingAssertionsTest < ActionController::TestCase
end
mount engine => "/shelf"
+
+ get "/shelf/foo", controller: "query_articles", action: "index"
end
end
@@ -154,6 +156,10 @@ class RoutingAssertionsTest < ActionController::TestCase
assert_match err.message, "This is a really bad msg"
end
+ def test_assert_recognizes_continue_to_recoginize_after_it_tried_engines
+ assert_recognizes({ controller: "query_articles", action: "index" }, "/shelf/foo")
+ end
+
def test_assert_routing
assert_routing("/articles", controller: "articles", action: "index")
end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 4222eb4eb7..fe314e26b1 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -4500,7 +4500,7 @@ class TestPortConstraints < ActionDispatch::IntegrationTest
get "/integer", to: ok, constraints: { port: 8080 }
get "/string", to: ok, constraints: { port: "8080" }
- get "/array", to: ok, constraints: { port: [8080] }
+ get "/array/:idx", to: ok, constraints: { port: [8080], idx: %w[first last] }
get "/regexp", to: ok, constraints: { port: /8080/ }
end
end
@@ -4529,7 +4529,10 @@ class TestPortConstraints < ActionDispatch::IntegrationTest
get "http://www.example.com/array"
assert_response :not_found
- get "http://www.example.com:8080/array"
+ get "http://www.example.com:8080/array/middle"
+ assert_response :not_found
+
+ get "http://www.example.com:8080/array/first"
assert_response :success
end
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index 8ac9502af9..90f2ee46ea 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -98,8 +98,8 @@ class RedirectSSLTest < SSLTest
end
class StrictTransportSecurityTest < SSLTest
- EXPECTED = "max-age=15552000"
- EXPECTED_WITH_SUBDOMAINS = "max-age=15552000; includeSubDomains"
+ EXPECTED = "max-age=31536000"
+ EXPECTED_WITH_SUBDOMAINS = "max-age=31536000; includeSubDomains"
def assert_hsts(expected, url: "https://example.org", hsts: { subdomains: true }, headers: {})
self.app = build_app ssl_options: { hsts: hsts }, headers: headers
diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
index 264844fc7d..de79c05657 100644
--- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -9,7 +9,7 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
new_test = DrivenBySeleniumWithChrome.new("x")
Rails.stub :root, Pathname.getwd do
- assert_equal "tmp/screenshots/x.png", new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
end
end
@@ -18,7 +18,7 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
Rails.stub :root, Pathname.getwd do
new_test.stub :passed?, false do
- assert_equal "tmp/screenshots/failures_x.png", new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/failures_x.png").to_s, new_test.send(:image_path)
end
end
end
@@ -29,7 +29,7 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
Rails.stub :root, Pathname.getwd do
new_test.stub :passed?, false do
new_test.stub :skipped?, true do
- assert_equal "tmp/screenshots/x.png", new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
end
end
end
@@ -59,11 +59,11 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
end
end
- test "image path returns the relative path from current directory" do
+ test "image path returns the absolute path from root" do
new_test = DrivenBySeleniumWithChrome.new("x")
Rails.stub :root, Pathname.getwd.join("..") do
- assert_equal "../tmp/screenshots/x.png", new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
end
end
end
diff --git a/actionpack/test/tmp/.gitignore b/actionpack/test/tmp/.gitignore
deleted file mode 100644
index e69de29bb2..0000000000
--- a/actionpack/test/tmp/.gitignore
+++ /dev/null
diff --git a/actionview/.gitignore b/actionview/.gitignore
index 0a04b29786..246aabbb7f 100644
--- a/actionview/.gitignore
+++ b/actionview/.gitignore
@@ -1,2 +1,5 @@
-/lib/assets/compiled
-/tmp
+/lib/assets/compiled/
+/log/
+/test/fixtures/public/absolute/
+/test/ujs/log/
+/tmp/
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index ea7f8b205e..001cc21181 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,41 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Enable select tag helper to mark `prompt` option as `selected` and/or `disabled` for `required`
+ field. Example:
+
+ select :post,
+ :category,
+ ["lifestyle", "programming", "spiritual"],
+ { selected: "", disabled: "", prompt: "Choose one" },
+ { required: true }
+
+ Placeholder option would be selected and disabled. The HTML produced:
+
+ <select required="required" name="post[category]" id="post_category">
+ <option disabled="disabled" selected="selected" value="">Choose one</option>
+ <option value="lifestyle">lifestyle</option>
+ <option value="programming">programming</option>
+ <option value="spiritual">spiritual</option></select>
+
+ For details see GH#32080
+
+ *Sergey Prikhodko*
+
+* Don't enforce UTF-8 by default
+
+ With the disabling of TLS 1.0 by most major websites, continuing to run
+ IE8 or lower becomes increasingly difficult so default to not enforcing
+ UTF-8 encoding as it's not relevant to other browsers.
+
+ *Andrew White*
+
+* Change translation key of `submit_tag` from `module_name_class_name` to `module_name/class_name`.
+
+ *Rui Onodera*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionview/CHANGELOG.md) for previous changes.
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
index b99137fcf6..49ee1a292b 100644
--- a/actionview/actionview.gemspec
+++ b/actionview/actionview.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Rendering framework putting the V in MVC (part of Rails)."
s.description = "Simple, battle-tested conventions and helpers for building web pages."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
index cc0e037428..2a8f5659e3 100644
--- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee
@@ -1,7 +1,8 @@
+#= require ./csp
#= require ./csrf
#= require ./event
-{ CSRFProtection, fire } = Rails
+{ cspNonce, CSRFProtection, fire } = Rails
AcceptHeaders =
'*': '*/*'
@@ -65,6 +66,7 @@ processResponse = (response, type) ->
try response = JSON.parse(response)
else if type.match(/\b(?:java|ecma)script\b/)
script = document.createElement('script')
+ script.nonce = cspNonce()
script.text = response
document.head.appendChild(script).parentNode.removeChild(script)
else if type.match(/\b(xml|html|svg)\b/)
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee
new file mode 100644
index 0000000000..8d2d6ce447
--- /dev/null
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee
@@ -0,0 +1,4 @@
+# Content-Security-Policy nonce for inline scripts
+cspNonce = Rails.cspNonce = ->
+ meta = document.querySelector('meta[name=csp-nonce]')
+ meta and meta.content
diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb
index 46f20c4277..8cc8013718 100644
--- a/actionview/lib/action_view/helpers.rb
+++ b/actionview/lib/action_view/helpers.rb
@@ -13,6 +13,7 @@ module ActionView #:nodoc:
autoload :CacheHelper
autoload :CaptureHelper
autoload :ControllerHelper
+ autoload :CspHelper
autoload :CsrfHelper
autoload :DateHelper
autoload :DebugHelper
@@ -46,6 +47,7 @@ module ActionView #:nodoc:
include CacheHelper
include CaptureHelper
include ControllerHelper
+ include CspHelper
include CsrfHelper
include DateHelper
include DebugHelper
diff --git a/actionview/lib/action_view/helpers/csp_helper.rb b/actionview/lib/action_view/helpers/csp_helper.rb
new file mode 100644
index 0000000000..e2e065c218
--- /dev/null
+++ b/actionview/lib/action_view/helpers/csp_helper.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActionView
+ # = Action View CSP Helper
+ module Helpers #:nodoc:
+ module CspHelper
+ # Returns a meta tag "csp-nonce" with the per-session nonce value
+ # for allowing inline <script> tags.
+ #
+ # <head>
+ # <%= csp_meta_tag %>
+ # </head>
+ #
+ # This is used by the Rails UJS helper to create dynamically
+ # loaded inline <script> elements.
+ #
+ def csp_meta_tag
+ if content_security_policy?
+ tag("meta", name: "csp-nonce", content: content_security_policy_nonce)
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb
index 52dff1f750..88ceba414b 100644
--- a/actionview/lib/action_view/helpers/debug_helper.rb
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -24,7 +24,7 @@ module ActionView
# created_at:
# </pre>
def debug(object)
- Marshal::dump(object)
+ Marshal.dump(object)
object = ERB::Util.html_escape(object.to_yaml)
content_tag(:pre, object, class: "debug_dump")
rescue # errors from Marshal or YAML
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index 15aa9ec2dd..afd49286e6 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -610,8 +610,8 @@ module ActionView
# unnecessary unless you support browsers without JavaScript.
# * <tt>:local</tt> - By default form submits are remote and unobtrusive XHRs.
# Disable remote submits with <tt>local: true</tt>.
- # * <tt>:skip_enforcing_utf8</tt> - By default a hidden field named +utf8+
- # is output to enforce UTF-8 submits. Set to true to skip the field.
+ # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name
+ # utf8 is not output.
# * <tt>:builder</tt> - Override the object used to build the form.
# * <tt>:id</tt> - Optional HTML id attribute.
# * <tt>:class</tt> - Optional HTML class attribute.
@@ -1519,10 +1519,10 @@ module ActionView
private
def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
- skip_enforcing_utf8: false, **options)
+ skip_enforcing_utf8: nil, **options)
html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html)
html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
- html_options[:enforce_utf8] = !skip_enforcing_utf8
+ html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil?
html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart)
@@ -2246,7 +2246,7 @@ module ActionView
@template.button_tag(value, options, &block)
end
- def emitted_hidden_id?
+ def emitted_hidden_id? # :nodoc:
@emitted_hidden_id ||= nil
end
@@ -2266,7 +2266,12 @@ module ActionView
end
defaults = []
- defaults << :"helpers.submit.#{object_name}.#{key}"
+ # Object is a model and it is not overwritten by as and scope option.
+ if object.respond_to?(:model_name) && object_name.to_s == model.downcase
+ defaults << :"helpers.submit.#{object.model_name.i18n_key}.#{key}"
+ else
+ defaults << :"helpers.submit.#{object_name}.#{key}"
+ end
defaults << :"helpers.submit.#{key}"
defaults << "#{key.to_s.humanize} #{model}"
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index e86e18dd78..5a8b8555a0 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -22,6 +22,8 @@ module ActionView
mattr_accessor :embed_authenticity_token_in_remote_forms
self.embed_authenticity_token_in_remote_forms = nil
+ mattr_accessor :default_enforce_utf8, default: true
+
# Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like
# ActionController::Base#url_for. The method for the form defaults to POST.
#
@@ -866,7 +868,7 @@ module ActionView
})
end
- if html_options.delete("enforce_utf8") { true }
+ if html_options.delete("enforce_utf8") { default_enforce_utf8 }
utf8_enforcer_tag + method_tag
else
method_tag
diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb
index dd2cd57ac3..acc50f8a62 100644
--- a/actionview/lib/action_view/helpers/javascript_helper.rb
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -63,6 +63,13 @@ module ActionView
# <%= javascript_tag defer: 'defer' do -%>
# alert('All is good')
# <% end -%>
+ #
+ # If you have a content security policy enabled then you can add an automatic
+ # nonce value by passing +nonce: true+ as part of +html_options+. Example:
+ #
+ # <%= javascript_tag nonce: true do -%>
+ # alert('All is good')
+ # <% end -%>
def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
content =
if block_given?
@@ -72,6 +79,10 @@ module ActionView
content_or_options_with_block
end
+ if html_options[:nonce] == true
+ html_options[:nonce] = content_security_policy_nonce
+ end
+
content_tag("script".freeze, javascript_cdata_section(content), html_options)
end
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index fed908fcdb..f1eca2268a 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -170,7 +170,11 @@ module ActionView
option_tags = tag_builder.content_tag_string("option", options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, value: "") + "\n" + option_tags
end
if value.blank? && options[:prompt]
- option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), value: "") + "\n" + option_tags
+ tag_options = { value: "" }.tap do |prompt_opts|
+ prompt_opts[:disabled] = true if options[:disabled] == ""
+ prompt_opts[:selected] = true if options[:selected] == ""
+ end
+ option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags
end
option_tags
end
diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb
index fcf96d2c9c..e81ca3aef0 100644
--- a/actionview/lib/action_view/helpers/tags/translator.rb
+++ b/actionview/lib/action_view/helpers/tags/translator.rb
@@ -16,13 +16,8 @@ module ActionView
translated_attribute || human_attribute_name
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :object_name, :method_and_value, :scope, :model
-
private
+ attr_reader :object_name, :method_and_value, :scope, :model
def i18n_default
if model
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index 73dfb267bb..12bdc642d4 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -9,6 +9,7 @@ module ActionView
config.action_view = ActiveSupport::OrderedOptions.new
config.action_view.embed_authenticity_token_in_remote_forms = nil
config.action_view.debug_missing_translation = true
+ config.action_view.default_enforce_utf8 = nil
config.eager_load_namespaces << ActionView
@@ -35,6 +36,15 @@ module ActionView
end
end
+ initializer "action_view.default_enforce_utf8" do |app|
+ ActiveSupport.on_load(:action_view) do
+ default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8)
+ unless default_enforce_utf8.nil?
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8
+ end
+ end
+ end
+
initializer "action_view.logger" do
ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
end
diff --git a/actionview/test/fixtures/public/.gitignore b/actionview/test/fixtures/public/.gitignore
deleted file mode 100644
index 312e635ee6..0000000000
--- a/actionview/test/fixtures/public/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-absolute/*
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index 79e52ccc47..94357d5f90 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -144,15 +144,13 @@ class DateHelperTest < ActionView::TestCase
rubinius_skip "Date is written in Ruby and relies on Fixnum#/"
jruby_skip "Date is written in Ruby and relies on Fixnum#/"
- klass = RUBY_VERSION > "2.4" ? Integer : Fixnum
-
# Make sure that we avoid {Integer,Fixnum}#/ (redefined by mathn)
- klass.send :private, :/
+ Integer.send :private, :/
from = Time.utc(2004, 6, 6, 21, 45, 0)
assert_distance_of_time_in_words(from)
ensure
- klass.send :public, :/
+ Integer.send :public, :/
end
def test_time_ago_in_words_passes_include_seconds
diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb
index 0d224d0c46..6b65d740eb 100644
--- a/actionview/test/template/form_helper/form_with_test.rb
+++ b/actionview/test/template/form_helper/form_with_test.rb
@@ -14,6 +14,16 @@ class FormWithTest < ActionView::TestCase
teardown do
ActionView::Helpers::FormHelper.form_with_generates_ids = @old_value
end
+
+ private
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
class FormWithActsLikeFormTagTest < FormWithTest
@@ -111,6 +121,24 @@ class FormWithActsLikeFormTagTest < FormWithTest
assert_predicate actual, :html_safe?
end
+ def test_form_with_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ actual = form_with
+ expected = whole_form("http://www.example.com", skip_enforcing_utf8: true)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
+ def test_form_with_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ actual = form_with
+ expected = whole_form("http://www.example.com", skip_enforcing_utf8: false)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
def test_form_with_with_block_in_erb
output_buffer = render_erb("<%= form_with(url: 'http://www.example.com') do %>Hello world!<% end %>")
@@ -190,6 +218,9 @@ class FormWithActsLikeFormForTest < FormWithTest
submit: "Save changes",
another_post: {
update: "Update your %{model}"
+ },
+ "blog/post": {
+ update: "Update your %{model}"
}
}
}
@@ -816,6 +847,34 @@ class FormWithActsLikeFormForTest < FormWithTest
assert_dom_equal expected, output_buffer
end
+ def test_form_with_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ form_with(scope: :post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", skip_enforcing_utf8: false) do
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_form_with_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ form_with(scope: :post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", skip_enforcing_utf8: true) do
+ "<input name='post[title]' type='text' value='Hello World' id='post_title' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_form_with_without_object
form_with(scope: :post, id: "create-post") do |f|
concat f.text_field(:title)
@@ -962,7 +1021,7 @@ class FormWithActsLikeFormForTest < FormWithTest
end
end
- def test_submit_with_object_and_nested_lookup
+ def test_submit_with_object_which_is_overwritten_by_scope_option
with_locale :submit do
form_with(model: @post, scope: :another_post) do |f|
concat f.submit
@@ -976,6 +1035,21 @@ class FormWithActsLikeFormForTest < FormWithTest
end
end
+ def test_submit_with_object_which_is_namespaced
+ blog_post = Blog::Post.new("And his name will be forty and four.", 44)
+ with_locale :submit do
+ form_with(model: blog_post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts/44", method: "patch") do
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_fields_with_attributes_not_on_model
form_with(model: @post) do |f|
concat f.fields(:comment) { |c|
@@ -2273,4 +2347,13 @@ class FormWithActsLikeFormForTest < FormWithTest
ensure
I18n.locale = old_locale
end
+
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index b8fad090c5..5244204e42 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -68,6 +68,9 @@ class FormHelperTest < ActionView::TestCase
submit: "Save changes",
another_post: {
update: "Update your %{model}"
+ },
+ "blog/post": {
+ update: "Update your %{model}"
}
}
}
@@ -1992,6 +1995,34 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_form_for_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ form_for(:post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: false) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_form_for_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ form_for(:post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_form_for_with_remote_in_html
form_for(@post, url: "/", html: { remote: true, id: "create-post", method: :patch }) do |f|
concat f.text_field(:title)
@@ -2271,7 +2302,7 @@ class FormHelperTest < ActionView::TestCase
end
end
- def test_submit_with_object_and_nested_lookup
+ def test_submit_with_object_which_is_overwritten_by_as_option
with_locale :submit do
form_for(@post, as: :another_post) do |f|
concat f.submit
@@ -2285,6 +2316,21 @@ class FormHelperTest < ActionView::TestCase
end
end
+ def test_submit_with_object_which_is_namespaced
+ blog_post = Blog::Post.new("And his name will be forty and four.", 44)
+ with_locale :submit do
+ form_for(blog_post) do |f|
+ concat f.submit
+ end
+
+ expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
def test_nested_fields_for
@comment.body = "Hello World"
form_for(@post) do |f|
@@ -3551,4 +3597,13 @@ class FormHelperTest < ActionView::TestCase
ensure
I18n.locale = old_locale
end
+
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
index f82eada869..8f796bdb83 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -511,6 +511,16 @@ class FormOptionsHelperTest < ActionView::TestCase
)
end
+ def test_required_select_with_default_and_selected_placeholder
+ assert_dom_equal(
+ ['<select required="required" name="post[category]" id="post_category"><option disabled="disabled" selected="selected" value="">Choose one</option>',
+ '<option value="lifestyle">lifestyle</option>',
+ '<option value="programming">programming</option>',
+ '<option value="spiritual">spiritual</option></select>'].join("\n"),
+ select(:post, :category, ["lifestyle", "programming", "spiritual"], { selected: "", disabled: "", prompt: "Choose one" }, { required: true })
+ )
+ end
+
def test_select_with_grouped_collection_as_nested_array
@post = Post.new
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index 0d9bf77f98..a3500a7eb3 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -152,6 +152,24 @@ class FormTagHelperTest < ActionView::TestCase
assert_predicate actual, :html_safe?
end
+ def test_form_tag_default_enforce_utf8_false
+ with_default_enforce_utf8 false do
+ actual = form_tag({})
+ expected = whole_form("http://www.example.com", enforce_utf8: false)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
+ def test_form_tag_default_enforce_utf8_true
+ with_default_enforce_utf8 true do
+ actual = form_tag({})
+ expected = whole_form("http://www.example.com", enforce_utf8: true)
+ assert_dom_equal expected, actual
+ assert_predicate actual, :html_safe?
+ end
+ end
+
def test_form_tag_with_block_in_erb
output_buffer = render_erb("<%= form_tag('http://www.example.com') do %>Hello world!<% end %>")
@@ -782,4 +800,13 @@ class FormTagHelperTest < ActionView::TestCase
def root_elem(rendered_content)
Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset
end
+
+ def with_default_enforce_utf8(value)
+ old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value
+
+ yield
+ ensure
+ ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value
+ end
end
diff --git a/actionview/test/tmp/.keep b/actionview/test/tmp/.keep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/actionview/test/tmp/.keep
+++ /dev/null
diff --git a/actionview/test/ujs/.gitignore b/actionview/test/ujs/.gitignore
deleted file mode 100644
index 31dbbff57c..0000000000
--- a/actionview/test/ujs/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/log
diff --git a/actionview/test/ujs/public/test/call-ajax.js b/actionview/test/ujs/public/test/call-ajax.js
index 49e64cad5c..4d0bfb0806 100644
--- a/actionview/test/ujs/public/test/call-ajax.js
+++ b/actionview/test/ujs/public/test/call-ajax.js
@@ -8,7 +8,6 @@ module('call-ajax', {
})
asyncTest('call ajax without "ajax:beforeSend"', 1, function() {
-
var link = $('#qunit-fixture a')
link.bindNative('click', function() {
Rails.ajax({
@@ -21,7 +20,7 @@ asyncTest('call ajax without "ajax:beforeSend"', 1, function() {
})
link.triggerNative('click')
- setTimeout(function() { start() }, 13)
+ setTimeout(function() { start() }, 50)
})
})()
diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb
index 7d1bab4b2a..48e9bcb65f 100644
--- a/actionview/test/ujs/server.rb
+++ b/actionview/test/ujs/server.rb
@@ -23,18 +23,30 @@ module UJS
config.public_file_server.enabled = true
config.logger = Logger.new(STDOUT)
config.log_level = :error
+
+ config.content_security_policy do |policy|
+ policy.default_src :self, :https
+ policy.font_src :self, :https, :data
+ policy.img_src :self, :https, :data
+ policy.object_src :none
+ policy.script_src :self, :https
+ policy.style_src :self, :https
+ end
+
+ config.content_security_policy_nonce_generator = ->(req) { SecureRandom.base64(16) }
end
end
module TestsHelper
def test_to(*names)
- names = ["/vendor/qunit.js", "settings"] + names
- names.map { |name| script_tag name }.join("\n").html_safe
- end
+ names = names.map { |name| "/test/#{name}.js" }
+ names = %w[/vendor/qunit.js /test/settings.js] + names
- def script_tag(src)
- src = "/test/#{src}.js" unless src.index("/")
- %(<script src="#{src}" type="text/javascript"></script>).html_safe
+ capture do
+ names.each do |name|
+ concat(javascript_include_tag(name))
+ end
+ end
end
end
@@ -56,7 +68,7 @@ class TestsController < ActionController::Base
elsif params[:iframe]
payload = JSON.generate(data).gsub("<", "&lt;").gsub(">", "&gt;")
html = <<-HTML
- <script>
+ <script nonce="#{request.content_security_policy_nonce}">
if (window.top && window.top !== window)
window.top.jQuery.event.trigger('iframe:loaded', #{payload})
</script>
diff --git a/actionview/test/ujs/views/layouts/application.html.erb b/actionview/test/ujs/views/layouts/application.html.erb
index c787e77b84..8f6f6fc17f 100644
--- a/actionview/test/ujs/views/layouts/application.html.erb
+++ b/actionview/test/ujs/views/layouts/application.html.erb
@@ -2,9 +2,10 @@
<html id="html">
<head>
<title><%= @title %></title>
+ <%= csp_meta_tag %>
<link href="/vendor/qunit.css" media="screen" rel="stylesheet" type="text/css" media="screen, projection" />
<script src="/vendor/jquery-2.2.0.js" type="text/javascript"></script>
- <script>
+ <%= javascript_tag nonce: true do %>
// This is for test in override.js.
// Must go before rails-ujs.
document.addEventListener('rails:attachBindings', function() {
@@ -15,8 +16,8 @@
e.preventDefault();
});
});
- </script>
- <%= script_tag "/rails-ujs.js" %>
+ <% end %>
+ <%= javascript_include_tag "/rails-ujs.js" %>
</head>
<body id="body">
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index ec99b5d975..c6a3ad8ade 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,3 +1,20 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Add support for timezones to Active Job
+
+ Record what was the current timezone in effect when the job was
+ enqueued and then restore when the job is executed in same way
+ that the current locale is recorded and restored.
+
+ *Andrew White*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
+* Add support to define custom argument serializers.
+
+ *Evgenii Pecherkin*, *Rafael Mendonça França*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md) for previous changes.
diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec
index 71e32f695b..be6292f737 100644
--- a/activejob/activejob.gemspec
+++ b/activejob/activejob.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Job framework with pluggable queues."
s.description = "Declare job classes that can be run by a variety of queueing backends."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb
index 626abaa767..01fab4d918 100644
--- a/activejob/lib/active_job.rb
+++ b/activejob/lib/active_job.rb
@@ -33,6 +33,7 @@ module ActiveJob
autoload :Base
autoload :QueueAdapters
+ autoload :Serializers
autoload :ConfiguredJob
autoload :TestCase
autoload :TestHelper
diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb
index de11e7fcb1..86bb0c5540 100644
--- a/activejob/lib/active_job/arguments.rb
+++ b/activejob/lib/active_job/arguments.rb
@@ -14,8 +14,8 @@ module ActiveJob
end
# Raised when an unsupported argument type is set as a job argument. We
- # currently support NilClass, Integer, Fixnum, Float, String, TrueClass, FalseClass,
- # Bignum, BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record).
+ # currently support NilClass, Integer, Float, String, TrueClass, FalseClass,
+ # BigDecimal, and objects that can be represented as GlobalIDs (ex: Active Record).
# Raised if you set the key for a Hash something else than a string or
# a symbol. Also raised when trying to serialize an object which can't be
# identified with a Global ID - such as an unpersisted Active Record model.
@@ -25,7 +25,6 @@ module ActiveJob
extend self
# :nodoc:
TYPE_WHITELIST = [ NilClass, String, Integer, Float, BigDecimal, TrueClass, FalseClass ]
- TYPE_WHITELIST.push(Fixnum, Bignum) unless 1.class == Integer
# Serializes a set of arguments. Whitelisted types are returned
# as-is. Arrays/Hashes are serialized element by element.
@@ -44,13 +43,24 @@ module ActiveJob
end
private
+
# :nodoc:
GLOBALID_KEY = "_aj_globalid".freeze
# :nodoc:
SYMBOL_KEYS_KEY = "_aj_symbol_keys".freeze
# :nodoc:
WITH_INDIFFERENT_ACCESS_KEY = "_aj_hash_with_indifferent_access".freeze
- private_constant :GLOBALID_KEY, :SYMBOL_KEYS_KEY, :WITH_INDIFFERENT_ACCESS_KEY
+ # :nodoc:
+ OBJECT_SERIALIZER_KEY = "_aj_serialized"
+
+ # :nodoc:
+ RESERVED_KEYS = [
+ GLOBALID_KEY, GLOBALID_KEY.to_sym,
+ SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
+ OBJECT_SERIALIZER_KEY, OBJECT_SERIALIZER_KEY.to_sym,
+ WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
+ ]
+ private_constant :RESERVED_KEYS
def serialize_argument(argument)
case argument
@@ -70,7 +80,7 @@ module ActiveJob
result[SYMBOL_KEYS_KEY] = symbol_keys
result
else
- raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
+ Serializers.serialize(argument)
end
end
@@ -85,6 +95,8 @@ module ActiveJob
when Hash
if serialized_global_id?(argument)
deserialize_global_id argument
+ elsif custom_serialized?(argument)
+ Serializers.deserialize(argument)
else
deserialize_hash(argument)
end
@@ -101,6 +113,10 @@ module ActiveJob
GlobalID::Locator.locate hash[GLOBALID_KEY]
end
+ def custom_serialized?(hash)
+ hash.key?(OBJECT_SERIALIZER_KEY)
+ end
+
def serialize_hash(argument)
argument.each_with_object({}) do |(key, value), hash|
hash[serialize_hash_key(key)] = serialize_argument(value)
@@ -117,14 +133,6 @@ module ActiveJob
result
end
- # :nodoc:
- RESERVED_KEYS = [
- GLOBALID_KEY, GLOBALID_KEY.to_sym,
- SYMBOL_KEYS_KEY, SYMBOL_KEYS_KEY.to_sym,
- WITH_INDIFFERENT_ACCESS_KEY, WITH_INDIFFERENT_ACCESS_KEY.to_sym,
- ]
- private_constant :RESERVED_KEYS
-
def serialize_hash_key(key)
case key
when *RESERVED_KEYS
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
index ae112abb2c..2b2a59e969 100644
--- a/activejob/lib/active_job/base.rb
+++ b/activejob/lib/active_job/base.rb
@@ -9,6 +9,7 @@ require "active_job/execution"
require "active_job/callbacks"
require "active_job/exceptions"
require "active_job/logging"
+require "active_job/timezones"
require "active_job/translation"
module ActiveJob #:nodoc:
@@ -59,6 +60,7 @@ module ActiveJob #:nodoc:
# * SerializationError - Error class for serialization errors.
class Base
include Core
+ include Serializers
include QueueAdapter
include QueueName
include QueuePriority
@@ -67,6 +69,7 @@ module ActiveJob #:nodoc:
include Callbacks
include Exceptions
include Logging
+ include Timezones
include Translation
ActiveSupport.run_load_hooks(:active_job, self)
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index 879746fc01..da841ae45b 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -31,6 +31,9 @@ module ActiveJob
# I18n.locale to be used during the job.
attr_accessor :locale
+
+ # Timezone to be used during the job.
+ attr_accessor :timezone
end
# These methods will be included into any Active Job object, adding
@@ -87,7 +90,8 @@ module ActiveJob
"priority" => priority,
"arguments" => serialize_arguments(arguments),
"executions" => executions,
- "locale" => I18n.locale.to_s
+ "locale" => I18n.locale.to_s,
+ "timezone" => Time.zone.try(:name)
}
end
@@ -125,6 +129,7 @@ module ActiveJob
self.serialized_arguments = job_data["arguments"]
self.executions = job_data["executions"]
self.locale = job_data["locale"] || I18n.locale.to_s
+ self.timezone = job_data["timezone"] || Time.zone.try(:name)
end
private
diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb
index f53b7eaee5..3312857ac7 100644
--- a/activejob/lib/active_job/logging.rb
+++ b/activejob/lib/active_job/logging.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/hash/transform_values"
require "active_support/core_ext/string/filters"
require "active_support/tagged_logging"
require "active_support/logger"
@@ -12,13 +11,13 @@ module ActiveJob
included do
cattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
- around_enqueue do |_, block, _|
+ around_enqueue do |_, block|
tag_logger do
block.call
end
end
- around_perform do |job, block, _|
+ around_perform do |job, block|
tag_logger(job.class.name, job.job_id) do
payload = { adapter: job.class.queue_adapter, job: job }
ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb
index 7b0742a6d2..d0294854d3 100644
--- a/activejob/lib/active_job/railtie.rb
+++ b/activejob/lib/active_job/railtie.rb
@@ -7,17 +7,28 @@ module ActiveJob
# = Active Job Railtie
class Railtie < Rails::Railtie # :nodoc:
config.active_job = ActiveSupport::OrderedOptions.new
+ config.active_job.custom_serializers = []
initializer "active_job.logger" do
ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
end
+ initializer "active_job.custom_serializers" do |app|
+ config.after_initialize do
+ custom_serializers = app.config.active_job.delete(:custom_serializers)
+ ActiveJob::Serializers.add_serializers custom_serializers
+ end
+ end
+
initializer "active_job.set_configs" do |app|
options = app.config.active_job
options.queue_adapter ||= :async
ActiveSupport.on_load(:active_job) do
- options.each { |k, v| send("#{k}=", v) }
+ options.each do |k, v|
+ k = "#{k}="
+ send(k, v) if respond_to? k
+ end
end
end
diff --git a/activejob/lib/active_job/serializers.rb b/activejob/lib/active_job/serializers.rb
new file mode 100644
index 0000000000..df66e66659
--- /dev/null
+++ b/activejob/lib/active_job/serializers.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+require "set"
+
+module ActiveJob
+ # The <tt>ActiveJob::Serializers</tt> module is used to store a list of known serializers
+ # and to add new ones. It also has helpers to serialize/deserialize objects.
+ module Serializers # :nodoc:
+ extend ActiveSupport::Autoload
+ extend ActiveSupport::Concern
+
+ autoload :ObjectSerializer
+ autoload :SymbolSerializer
+ autoload :DurationSerializer
+ autoload :DateTimeSerializer
+ autoload :DateSerializer
+ autoload :TimeWithZoneSerializer
+ autoload :TimeSerializer
+
+ mattr_accessor :_additional_serializers
+ self._additional_serializers = Set.new
+
+ class << self
+ # Returns serialized representative of the passed object.
+ # Will look up through all known serializers.
+ # Raises <tt>ActiveJob::SerializationError</tt> if it can't find a proper serializer.
+ def serialize(argument)
+ serializer = serializers.detect { |s| s.serialize?(argument) }
+ raise SerializationError.new("Unsupported argument type: #{argument.class.name}") unless serializer
+ serializer.serialize(argument)
+ end
+
+ # Returns deserialized object.
+ # Will look up through all known serializers.
+ # If no serializer found will raise <tt>ArgumentError</tt>.
+ def deserialize(argument)
+ serializer_name = argument[Arguments::OBJECT_SERIALIZER_KEY]
+ raise ArgumentError, "Serializer name is not present in the argument: #{argument.inspect}" unless serializer_name
+
+ serializer = serializer_name.safe_constantize
+ raise ArgumentError, "Serializer #{serializer_name} is not known" unless serializer
+
+ serializer.deserialize(argument)
+ end
+
+ # Returns list of known serializers.
+ def serializers
+ self._additional_serializers
+ end
+
+ # Adds new serializers to a list of known serializers.
+ def add_serializers(*new_serializers)
+ self._additional_serializers += new_serializers.flatten
+ end
+ end
+
+ add_serializers SymbolSerializer,
+ DurationSerializer,
+ DateTimeSerializer,
+ DateSerializer,
+ TimeWithZoneSerializer,
+ TimeSerializer
+ end
+end
diff --git a/activejob/lib/active_job/serializers/date_serializer.rb b/activejob/lib/active_job/serializers/date_serializer.rb
new file mode 100644
index 0000000000..e995d30faa
--- /dev/null
+++ b/activejob/lib/active_job/serializers/date_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DateSerializer < ObjectSerializer # :nodoc:
+ def serialize(date)
+ super("value" => date.iso8601)
+ end
+
+ def deserialize(hash)
+ Date.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ Date
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/date_time_serializer.rb b/activejob/lib/active_job/serializers/date_time_serializer.rb
new file mode 100644
index 0000000000..fe780a1978
--- /dev/null
+++ b/activejob/lib/active_job/serializers/date_time_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DateTimeSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ DateTime.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ DateTime
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/duration_serializer.rb b/activejob/lib/active_job/serializers/duration_serializer.rb
new file mode 100644
index 0000000000..715fe27a5c
--- /dev/null
+++ b/activejob/lib/active_job/serializers/duration_serializer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class DurationSerializer < ObjectSerializer # :nodoc:
+ def serialize(duration)
+ super("value" => duration.value, "parts" => Arguments.serialize(duration.parts))
+ end
+
+ def deserialize(hash)
+ value = hash["value"]
+ parts = Arguments.deserialize(hash["parts"])
+
+ klass.new(value, parts)
+ end
+
+ private
+
+ def klass
+ ActiveSupport::Duration
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb
new file mode 100644
index 0000000000..1dfd1e44be
--- /dev/null
+++ b/activejob/lib/active_job/serializers/object_serializer.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ # Base class for serializing and deserializing custom objects.
+ #
+ # Example:
+ #
+ # class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
+ # def serialize(money)
+ # super("amount" => money.amount, "currency" => money.currency)
+ # end
+ #
+ # def deserialize(hash)
+ # Money.new(hash["amount"], hash["currency"])
+ # end
+ #
+ # private
+ #
+ # def klass
+ # Money
+ # end
+ # end
+ class ObjectSerializer
+ include Singleton
+
+ class << self
+ delegate :serialize?, :serialize, :deserialize, to: :instance
+ end
+
+ # Determines if an argument should be serialized by a serializer.
+ def serialize?(argument)
+ argument.is_a?(klass)
+ end
+
+ # Serializes an argument to a JSON primitive type.
+ def serialize(hash)
+ { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash)
+ end
+
+ # Deserilizes an argument form a JSON primiteve type.
+ def deserialize(_argument)
+ raise NotImplementedError
+ end
+
+ private
+
+ # The class of the object that will be serialized.
+ def klass # :doc:
+ raise NotImplementedError
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/symbol_serializer.rb b/activejob/lib/active_job/serializers/symbol_serializer.rb
new file mode 100644
index 0000000000..7e1f9553a2
--- /dev/null
+++ b/activejob/lib/active_job/serializers/symbol_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class SymbolSerializer < ObjectSerializer # :nodoc:
+ def serialize(argument)
+ super("value" => argument.to_s)
+ end
+
+ def deserialize(argument)
+ argument["value"].to_sym
+ end
+
+ private
+
+ def klass
+ Symbol
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/time_serializer.rb b/activejob/lib/active_job/serializers/time_serializer.rb
new file mode 100644
index 0000000000..fe20772f35
--- /dev/null
+++ b/activejob/lib/active_job/serializers/time_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class TimeSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ Time.iso8601(hash["value"])
+ end
+
+ private
+
+ def klass
+ Time
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/serializers/time_with_zone_serializer.rb b/activejob/lib/active_job/serializers/time_with_zone_serializer.rb
new file mode 100644
index 0000000000..43017fc75b
--- /dev/null
+++ b/activejob/lib/active_job/serializers/time_with_zone_serializer.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Serializers
+ class TimeWithZoneSerializer < ObjectSerializer # :nodoc:
+ def serialize(time)
+ super("value" => time.iso8601)
+ end
+
+ def deserialize(hash)
+ Time.iso8601(hash["value"]).in_time_zone
+ end
+
+ private
+
+ def klass
+ ActiveSupport::TimeWithZone
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/timezones.rb b/activejob/lib/active_job/timezones.rb
new file mode 100644
index 0000000000..ac018eb752
--- /dev/null
+++ b/activejob/lib/active_job/timezones.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module ActiveJob
+ module Timezones #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ around_perform do |job, block|
+ Time.use_zone(job.timezone, &block)
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/translation.rb b/activejob/lib/active_job/translation.rb
index fb45c80d67..0fd9b9fc06 100644
--- a/activejob/lib/active_job/translation.rb
+++ b/activejob/lib/active_job/translation.rb
@@ -5,7 +5,7 @@ module ActiveJob
extend ActiveSupport::Concern
included do
- around_perform do |job, block, _|
+ around_perform do |job, block|
I18n.with_locale(job.locale, &block)
end
end
diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb
index 7e7f854da0..e5f1f087fe 100644
--- a/activejob/test/cases/argument_serialization_test.rb
+++ b/activejob/test/cases/argument_serialization_test.rb
@@ -13,6 +13,9 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
[ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
"a", true, false, BigDecimal(5),
+ :a, 1.day, Date.new(2001, 2, 3), Time.new(2002, 10, 31, 2, 2, 2, "+02:00"),
+ DateTime.new(2001, 2, 3, 4, 5, 6, "+03:00"),
+ ActiveSupport::TimeWithZone.new(Time.utc(1999, 12, 31, 23, 59, 59), ActiveSupport::TimeZone["UTC"]),
[ 1, "a" ],
{ "a" => 1 }
].each do |arg|
@@ -21,7 +24,7 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
end
end
- [ :a, Object.new, self, Person.find("5").to_gid ].each do |arg|
+ [ Object.new, self, Person.find("5").to_gid ].each do |arg|
test "does not serialize #{arg.class}" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ arg ]
@@ -46,6 +49,49 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
assert_arguments_roundtrip([a: 1, "b" => 2])
end
+ test "serialize a hash" do
+ symbol_key = { a: 1 }
+ string_key = { "a" => 1 }
+ indifferent_access = { a: 1 }.with_indifferent_access
+
+ assert_equal(
+ { "a" => 1, "_aj_symbol_keys" => ["a"] },
+ ActiveJob::Arguments.serialize([symbol_key]).first
+ )
+ assert_equal(
+ { "a" => 1, "_aj_symbol_keys" => [] },
+ ActiveJob::Arguments.serialize([string_key]).first
+ )
+ assert_equal(
+ { "a" => 1, "_aj_hash_with_indifferent_access" => true },
+ ActiveJob::Arguments.serialize([indifferent_access]).first
+ )
+ end
+
+ test "deserialize a hash" do
+ symbol_key = { "a" => 1, "_aj_symbol_keys" => ["a"] }
+ string_key = { "a" => 1, "_aj_symbol_keys" => [] }
+ another_string_key = { "a" => 1 }
+ indifferent_access = { "a" => 1, "_aj_hash_with_indifferent_access" => true }
+
+ assert_equal(
+ { a: 1 },
+ ActiveJob::Arguments.deserialize([symbol_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([string_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([another_string_key]).first
+ )
+ assert_equal(
+ { "a" => 1 },
+ ActiveJob::Arguments.deserialize([indifferent_access]).first
+ )
+ end
+
test "should maintain hash with indifferent access" do
symbol_key = { a: 1 }
string_key = { "a" => 1 }
@@ -56,6 +102,14 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
assert_instance_of ActiveSupport::HashWithIndifferentAccess, perform_round_trip([indifferent_access]).first
end
+ test "should maintain time with zone" do
+ Time.use_zone "Alaska" do
+ time_with_zone = Time.new(2002, 10, 31, 2, 2, 2).in_time_zone
+ assert_instance_of ActiveSupport::TimeWithZone, perform_round_trip([time_with_zone]).first
+ assert_arguments_unchanged time_with_zone
+ end
+ end
+
test "should disallow non-string/symbol hash keys" do
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ { 1 => 2 } ]
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
index 440051c427..5c9994508e 100644
--- a/activejob/test/cases/job_serialization_test.rb
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -54,4 +54,11 @@ class JobSerializationTest < ActiveSupport::TestCase
job.provider_job_id = "some value set by adapter"
assert_equal job.provider_job_id, job.serialize["provider_job_id"]
end
+
+ test "serialize stores the current timezone" do
+ Time.use_zone "Hawaii" do
+ job = HelloJob.new
+ assert_equal "Hawaii", job.serialize["timezone"]
+ end
+ end
end
diff --git a/activejob/test/cases/serializers_test.rb b/activejob/test/cases/serializers_test.rb
new file mode 100644
index 0000000000..bee0c061bd
--- /dev/null
+++ b/activejob/test/cases/serializers_test.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require "helper"
+require "active_job/serializers"
+
+class SerializersTest < ActiveSupport::TestCase
+ class DummyValueObject
+ attr_accessor :value
+
+ def initialize(value)
+ @value = value
+ end
+
+ def ==(other)
+ self.value == other.value
+ end
+ end
+
+ class DummySerializer < ActiveJob::Serializers::ObjectSerializer
+ def serialize(object)
+ super({ "value" => object.value })
+ end
+
+ def deserialize(hash)
+ DummyValueObject.new(hash["value"])
+ end
+
+ private
+
+ def klass
+ DummyValueObject
+ end
+ end
+
+ setup do
+ @value_object = DummyValueObject.new 123
+ @original_serializers = ActiveJob::Serializers.serializers
+ end
+
+ teardown do
+ ActiveJob::Serializers._additional_serializers = @original_serializers
+ end
+
+ test "can't serialize unknown object" do
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Serializers.serialize @value_object
+ end
+ end
+
+ test "will serialize objects with serializers registered" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+
+ assert_equal(
+ { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 },
+ ActiveJob::Serializers.serialize(@value_object)
+ )
+ end
+
+ test "won't deserialize unknown hash" do
+ hash = { "_dummy_serializer" => 123, "_aj_symbol_keys" => [] }
+ error = assert_raises(ArgumentError) do
+ ActiveJob::Serializers.deserialize(hash)
+ end
+ assert_equal(
+ 'Serializer name is not present in the argument: {"_dummy_serializer"=>123, "_aj_symbol_keys"=>[]}',
+ error.message
+ )
+ end
+
+ test "won't deserialize unknown serializer" do
+ hash = { "_aj_serialized" => "DoNotExist", "value" => 123 }
+ error = assert_raises(ArgumentError) do
+ ActiveJob::Serializers.deserialize(hash)
+ end
+ assert_equal(
+ "Serializer DoNotExist is not known",
+ error.message
+ )
+ end
+
+ test "will deserialize know serialized objects" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ hash = { "_aj_serialized" => "SerializersTest::DummySerializer", "value" => 123 }
+ assert_equal DummyValueObject.new(123), ActiveJob::Serializers.deserialize(hash)
+ end
+
+ test "adds new serializer" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ assert ActiveJob::Serializers.serializers.include?(DummySerializer)
+ end
+
+ test "can't add serializer with the same key twice" do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ assert_no_difference(-> { ActiveJob::Serializers.serializers.size }) do
+ ActiveJob::Serializers.add_serializers DummySerializer
+ end
+ end
+end
diff --git a/activejob/test/cases/timezones_test.rb b/activejob/test/cases/timezones_test.rb
new file mode 100644
index 0000000000..e2095b020d
--- /dev/null
+++ b/activejob/test/cases/timezones_test.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "helper"
+require "jobs/timezone_dependent_job"
+
+class TimezonesTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ end
+
+ test "it performs the job in the given timezone" do
+ job = TimezoneDependentJob.new("2018-01-01T00:00:00Z")
+ job.timezone = "London"
+ job.perform_now
+
+ assert_equal "Happy New Year!", JobBuffer.last_value
+
+ job = TimezoneDependentJob.new("2018-01-01T00:00:00Z")
+ job.timezone = "Eastern Time (US & Canada)"
+ job.perform_now
+
+ assert_equal "Just 5 hours to go", JobBuffer.last_value
+ end
+end
diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb
index 32ef485c45..7a95d3d039 100644
--- a/activejob/test/integration/queuing_test.rb
+++ b/activejob/test/integration/queuing_test.rb
@@ -110,6 +110,22 @@ class QueuingTest < ActiveSupport::TestCase
end
end
+ test "current timezone is kept while running perform_later" do
+ skip if adapter_is?(:inline)
+
+ begin
+ current_zone = Time.zone
+ Time.zone = "Hawaii"
+
+ TestJob.perform_later @id
+ wait_for_jobs_to_finish_for(5.seconds)
+ assert job_executed
+ assert_equal "Hawaii", job_executed_in_timezone
+ ensure
+ Time.zone = current_zone
+ end
+ end
+
test "should run job with higher priority first" do
skip unless adapter_is?(:delayed_job, :que)
diff --git a/activejob/test/jobs/timezone_dependent_job.rb b/activejob/test/jobs/timezone_dependent_job.rb
new file mode 100644
index 0000000000..41f473d533
--- /dev/null
+++ b/activejob/test/jobs/timezone_dependent_job.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "../support/job_buffer"
+
+class TimezoneDependentJob < ActiveJob::Base
+ def perform(now)
+ now = now.in_time_zone
+ new_year = localtime(2018, 1, 1)
+
+ if now >= new_year
+ JobBuffer.add("Happy New Year!")
+ else
+ JobBuffer.add("Just #{(new_year - now).div(3600)} hours to go")
+ end
+ end
+
+ private
+
+ def localtime(*args)
+ Time.zone ? Time.zone.local(*args) : Time.utc(*args)
+ end
+end
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
index 7ea78c3350..b56dd3e591 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -21,6 +21,7 @@ class TestJob < ActiveJob::Base
File.open(Rails.root.join("tmp/\#{x}.new"), "wb+") do |f|
f.write Marshal.dump({
"locale" => I18n.locale.to_s || "en",
+ "timezone" => Time.zone.try(:name) || "UTC",
"executed_at" => Time.now.to_r
})
end
diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb
index f02a32a38e..3d9b265b66 100644
--- a/activejob/test/support/integration/test_case_helpers.rb
+++ b/activejob/test/support/integration/test_case_helpers.rb
@@ -62,4 +62,8 @@ module TestCaseHelpers
def job_executed_in_locale(id = @id)
job_data(id)["locale"]
end
+
+ def job_executed_in_timezone(id = @id)
+ job_data(id)["timezone"]
+ end
end
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 13ea7d0bf3..8a3578229a 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,12 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Do not lose all multiple `:includes` with options in serialization.
+
+ *Mike Mangino*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index a070a2898c..7be466dc4c 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "A toolkit for building modeling frameworks (part of Rails)."
s.description = "A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/activemodel/lib/active_model/attribute.rb b/activemodel/lib/active_model/attribute.rb
index 27f3e38e31..3f19cda07b 100644
--- a/activemodel/lib/active_model/attribute.rb
+++ b/activemodel/lib/active_model/attribute.rb
@@ -133,10 +133,6 @@ module ActiveModel
end
protected
-
- attr_reader :original_attribute
- alias_method :assigned?, :original_attribute
-
def original_value_for_database
if assigned?
original_attribute.original_value_for_database
@@ -146,6 +142,9 @@ module ActiveModel
end
private
+ attr_reader :original_attribute
+ alias :assigned? :original_attribute
+
def initialize_dup(other)
if defined?(@value) && @value.duplicable?
@value = @value.dup
diff --git a/activemodel/lib/active_model/attribute/user_provided_default.rb b/activemodel/lib/active_model/attribute/user_provided_default.rb
index a5dc6188d2..9dc16e882d 100644
--- a/activemodel/lib/active_model/attribute/user_provided_default.rb
+++ b/activemodel/lib/active_model/attribute/user_provided_default.rb
@@ -44,8 +44,7 @@ module ActiveModel
end
end
- protected
-
+ private
attr_reader :user_provided_value
end
end
diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb
index f55613ecd5..8e92c8807f 100644
--- a/activemodel/lib/active_model/attribute_mutation_tracker.rb
+++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb
@@ -69,13 +69,8 @@ module ActiveModel
forced_changes << attr_name.to_s
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :attributes, :forced_changes
-
private
+ attr_reader :attributes, :forced_changes
def attr_names
attributes.keys
diff --git a/activemodel/lib/active_model/attribute_set/builder.rb b/activemodel/lib/active_model/attribute_set/builder.rb
index bf2d06b48a..2b1c2206ec 100644
--- a/activemodel/lib/active_model/attribute_set/builder.rb
+++ b/activemodel/lib/active_model/attribute_set/builder.rb
@@ -90,9 +90,6 @@ module ActiveModel
end
protected
-
- attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
-
def materialize
unless @materialized
values.each_key { |key| self[key] }
@@ -105,6 +102,7 @@ module ActiveModel
end
private
+ attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
def assign_default_value(name)
type = additional_types.fetch(name, types[name])
diff --git a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
index 4ea945b956..ea1efc160e 100644
--- a/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
+++ b/activemodel/lib/active_model/attribute_set/yaml_encoder.rb
@@ -33,8 +33,7 @@ module ActiveModel
end
end
- protected
-
+ private
attr_reader :default_types
end
end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 47cb81bee5..c4b7b32291 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -179,7 +179,7 @@ module ActiveModel
return unless includes = options[:include]
unless includes.is_a?(Hash)
- includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
+ includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
end
includes.each do |association, opts|
diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb
index 8cecc16d0f..8ec5deedc4 100644
--- a/activemodel/lib/active_model/type/date.rb
+++ b/activemodel/lib/active_model/type/date.rb
@@ -42,7 +42,7 @@ module ActiveModel
end
def new_date(year, mon, mday)
- if year && year != 0
+ unless year.nil? || (year == 0 && mon == 0 && mday == 0)
::Date.new(year, mon, mday) rescue nil
end
end
diff --git a/activemodel/lib/active_model/type/integer.rb b/activemodel/lib/active_model/type/integer.rb
index fe396998a3..da74aaa3c5 100644
--- a/activemodel/lib/active_model/type/integer.rb
+++ b/activemodel/lib/active_model/type/integer.rb
@@ -31,13 +31,8 @@ module ActiveModel
result
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :range
-
private
+ attr_reader :range
def cast_value(value)
case value
diff --git a/activemodel/lib/active_model/type/registry.rb b/activemodel/lib/active_model/type/registry.rb
index 7272d7b0c5..a19dc0f011 100644
--- a/activemodel/lib/active_model/type/registry.rb
+++ b/activemodel/lib/active_model/type/registry.rb
@@ -23,13 +23,8 @@ module ActiveModel
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :registrations
-
private
+ attr_reader :registrations
def registration_klass
Registration
@@ -59,10 +54,7 @@ module ActiveModel
type_name == name
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :name, :block
end
end
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index f35e4dec7f..ea3a6b52ab 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -58,13 +58,8 @@ module ActiveModel
klass.send(:attr_writer, *attr_writers)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :attributes
-
private
+ attr_reader :attributes
def convert_to_reader_name(method_name)
method_name.to_s.chomp("=")
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index 0b9b5ce6a1..bafb8e2106 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -32,7 +32,7 @@ module ActiveModel
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
# possible values in the range for equality, which is slower but more accurate.
# <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
# endpoints, which is fast but is only accurate on Numeric, Time, Date,
diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb
index 5ecf0a69c4..448f8587fd 100644
--- a/activemodel/test/cases/attribute_assignment_test.rb
+++ b/activemodel/test/cases/attribute_assignment_test.rb
@@ -18,10 +18,7 @@ class AttributeAssignmentTest < ActiveModel::TestCase
raise ErrorFromAttributeWriter
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_writer :metadata
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index fe351f922d..cb6a8c43d5 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_support/core_ext/string/strip"
require "yaml"
class ErrorsTest < ActiveModel::TestCase
@@ -406,7 +405,7 @@ class ErrorsTest < ActiveModel::TestCase
end
test "errors are backward compatible with the Rails 4.2 format" do
- yaml = <<-CODE.strip_heredoc
+ yaml = <<~CODE
--- !ruby/object:ActiveModel::Errors
base: &1 !ruby/object:ErrorsTest::Person
errors: !ruby/object:ActiveModel::Errors
diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb
index 9002982e7f..6826d2bbd1 100644
--- a/activemodel/test/cases/serialization_test.rb
+++ b/activemodel/test/cases/serialization_test.rb
@@ -174,4 +174,11 @@ class SerializationTest < ActiveModel::TestCase
{ "name" => "Sue", "email" => "sue@example.com", "gender" => "female" }] }
assert_equal expected, @user.serializable_hash(include: [{ address: { only: "street" } }, :friends])
end
+
+ def test_all_includes_with_options
+ expected = { "email" => "david@example.com", "gender" => "male", "name" => "David",
+ "address" => { "street" => "123 Lane" },
+ "friends" => [{ "name" => "Joe" }, { "name" => "Sue" }] }
+ assert_equal expected, @user.serializable_hash(include: [address: { only: "street" }, friends: { only: "name" }])
+ end
end
diff --git a/activerecord/.gitignore b/activerecord/.gitignore
new file mode 100644
index 0000000000..884ee009eb
--- /dev/null
+++ b/activerecord/.gitignore
@@ -0,0 +1,3 @@
+/sqlnet.log
+/test/config.yml
+/test/fixtures/*.sqlite*
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index d73c0859ee..92c20ec79f 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,35 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Fix `#columsn_for_distinct` of MySQL and PostgreSQL to make
+ `ActiveRecord::FinderMethods#limited_ids_for` use correct primary key values
+ even if `ORDER BY` columns include other table's primary key.
+
+ Fixes #28364.
+
+ *Takumi Kagiyama*
+
+* Make `reflection.klass` raise if `polymorphic?` not to be misused.
+
+ Fixes #31876.
+
+ *Ryuta Kamizono*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
+* Deprecate `update_attributes`/`!` in favor of `update`/`!`.
+
+ *Eddie Lebow*
+
+* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in
+ ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database.
+
+ *DHH*
+
+* Add `Relation#pick` as short-hand for single-value plucks.
+
+ *DHH*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 8e42a11df4..b43e7c50f5 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Object-relational mapper framework (part of Rails)."
s.description = "Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/activerecord/examples/.gitignore b/activerecord/examples/.gitignore
deleted file mode 100644
index 0dfc1cb7fb..0000000000
--- a/activerecord/examples/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-performance.sql
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index b4377ad6be..d43378c64f 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -163,6 +163,7 @@ module ActiveRecord
"active_record/tasks/postgresql_database_tasks"
end
+ autoload :TestDatabases, "active_record/test_databases"
autoload :TestFixtures, "active_record/fixtures"
def self.eager_load!
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 7667c6ed8b..364f1fe74f 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -156,9 +156,9 @@ module ActiveRecord
reset
end
- # We can't dump @reflection since it contains the scope proc
+ # We can't dump @reflection and @through_reflection since it contains the scope proc
def marshal_dump
- ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
+ ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
[@reflection.name, ivars]
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 11967e0571..ad41ec8d2f 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -46,13 +46,9 @@ module ActiveRecord
binds
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :value_transformation
- private
def join(table, constraint)
table.create_join(table, table.create_on(constraint))
end
@@ -135,7 +131,7 @@ module ActiveRecord
item = eval_scope(reflection, scope_chain_item, owner)
if scope_chain_item == chain_head.scope
- scope.merge! item.except(:where, :includes)
+ scope.merge! item.except(:where, :includes, :unscope, :order)
end
reflection.all_includes do
@@ -144,7 +140,7 @@ module ActiveRecord
scope.unscope!(*item.unscope_values)
scope.where_clause += item.where_clause
- scope.order_values |= item.order_values
+ scope.order_values = item.order_values | scope.order_values
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 221c791bf8..c36386ec7e 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -58,7 +58,7 @@ module ActiveRecord
tables.first
end
- protected
+ private
attr_reader :alias_tracker
end
end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 59320431ee..1ea0aeac3a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -169,7 +169,7 @@ module ActiveRecord
owners.flat_map { |owner| owner.association(reflection.name).target }
end
- protected
+ private
attr_reader :owners, :reflection
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index ec6f0df484..71c8f6df58 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -27,10 +27,9 @@ module ActiveRecord
end
end
- protected
+ private
attr_reader :owners, :reflection, :preload_scope, :model, :klass
- private
# The name of the key on the associated records
def association_key_name
reflection.join_primary_key(klass)
@@ -118,7 +117,7 @@ module ActiveRecord
scope = klass.scope_for_association
if reflection.type
- scope.where!(reflection.type => model.base_class.sti_name)
+ scope.where!(reflection.type => model.base_class.name)
end
scope.merge!(reflection_scope) if reflection.scope
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index c77790dc9a..842f407517 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -432,15 +432,12 @@ module ActiveRecord
@attributes.accessed
end
- protected
-
- def attribute_method?(attr_name) # :nodoc:
+ private
+ def attribute_method?(attr_name)
# We check defined? because Syck calls respond_to? before actually calling initialize.
defined?(@attributes) && @attributes.key?(attr_name)
end
- private
-
def arel_attributes_with_values_for_create(attribute_names)
arel_attributes_with_values(attributes_for_create(attribute_names))
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index d8fc046e10..2907547634 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -131,7 +131,7 @@ module ActiveRecord
def suppress_composite_primary_key(pk)
return pk unless pk.is_a?(Array)
- warn <<-WARNING.strip_heredoc
+ warn <<~WARNING
WARNING: Active Record does not support composite primary key.
#{table_name} has composite primary key. Composite primary key is ignored.
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index ebc2baed34..6e0e90f39c 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -7,7 +7,7 @@ module ActiveRecord
class ColumnNotSerializableError < StandardError
def initialize(name, type)
- super <<-EOS.strip_heredoc
+ super <<~EOS
Column `#{name}` of type #{type.class} does not support `serialize` feature.
Usually it means that you are trying to use `serialize`
on a column that already implements serialization natively.
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index b7ad944cec..cc99401390 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -9,7 +9,6 @@ require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/hash/deep_merge"
require "active_support/core_ext/hash/slice"
-require "active_support/core_ext/hash/transform_values"
require "active_support/core_ext/string/behavior"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/module/introspection"
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 4a191d337c..22abdee4b8 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/string/strip"
-
module ActiveRecord
module ConnectionAdapters
class AbstractAdapter
@@ -66,7 +64,7 @@ module ActiveRecord
end
def visit_ForeignKeyDefinition(o)
- sql = <<-SQL.strip_heredoc
+ sql = +<<~SQL
CONSTRAINT #{quote_column_name(o.name)}
FOREIGN KEY (#{quote_column_name(o.column)})
REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
@@ -133,7 +131,7 @@ module ActiveRecord
when :cascade then "ON #{action} CASCADE"
when :restrict then "ON #{action} RESTRICT"
else
- raise ArgumentError, <<-MSG.strip_heredoc
+ raise ArgumentError, <<~MSG
'#{dependency}' is not supported for :on_update or :on_delete.
Supported values are: :nullify, :cascade, :restrict
MSG
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 0594b4b485..584a86da21 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -151,13 +151,8 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
-
private
+ attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
def as_options(value)
value.is_a?(Hash) ? value : {}
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 1fb0bd8d56..fbedddd7f9 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -11,8 +11,6 @@ require "active_record/connection_adapters/mysql/schema_dumper"
require "active_record/connection_adapters/mysql/schema_statements"
require "active_record/connection_adapters/mysql/type_metadata"
-require "active_support/core_ext/string/strip"
-
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
@@ -284,7 +282,7 @@ module ActiveRecord
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name)
- query_value(<<-SQL.strip_heredoc, "SCHEMA").presence
+ query_value(<<~SQL, "SCHEMA").presence
SELECT table_comment
FROM information_schema.tables
WHERE table_schema = #{scope[:schema]}
@@ -392,7 +390,7 @@ module ActiveRecord
scope = quoted_scope(table_name)
- fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA")
+ fk_info = exec_query(<<~SQL, "SCHEMA")
SELECT fk.referenced_table_name AS 'to_table',
fk.referenced_column_name AS 'primary_key',
fk.column_name AS 'column',
@@ -480,7 +478,7 @@ module ActiveRecord
scope = quoted_scope(table_name)
- query_values(<<-SQL.strip_heredoc, "SCHEMA")
+ query_values(<<~SQL, "SCHEMA")
SELECT column_name
FROM information_schema.key_column_usage
WHERE constraint_name = 'PRIMARY'
@@ -515,7 +513,7 @@ module ActiveRecord
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super, *order_columns].join(", ")
+ (order_columns << super).join(", ")
end
def strict_mode?
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index 469ef3f5a0..3ccc7271ab 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -20,10 +20,9 @@ module ActiveRecord
end
end
- protected
+ private
attr_reader :max_identifier_length
- private
def sequence_name_from_parts(table_name, column_name, suffix)
over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 542ca75d3e..247a25054e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -5,6 +5,7 @@ require "active_record/connection_adapters/postgresql/oid/bit"
require "active_record/connection_adapters/postgresql/oid/bit_varying"
require "active_record/connection_adapters/postgresql/oid/bytea"
require "active_record/connection_adapters/postgresql/oid/cidr"
+require "active_record/connection_adapters/postgresql/oid/date"
require "active_record/connection_adapters/postgresql/oid/date_time"
require "active_record/connection_adapters/postgresql/oid/decimal"
require "active_record/connection_adapters/postgresql/oid/enum"
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
index 587e95d192..e9a79526f9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -43,10 +43,7 @@ module ActiveRecord
/\A[0-9A-F]*\Z/i.match?(value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :value
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
new file mode 100644
index 0000000000..24a1daa95a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Date < Type::Date # :nodoc:
+ def cast_value(value)
+ case value
+ when "infinity" then ::Float::INFINITY
+ when "-infinity" then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 7999434ad8..45b230f0f9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -115,7 +115,7 @@ module ActiveRecord
if indkey.include?(0)
columns = expressions
else
- columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact
+ columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact
SELECT a.attnum, a.attname
FROM pg_attribute a
WHERE a.attrelid = #{oid}
@@ -158,7 +158,7 @@ module ActiveRecord
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name, type: "BASE TABLE")
if scope[:name]
- query_value(<<-SQL.strip_heredoc, "SCHEMA")
+ query_value(<<~SQL, "SCHEMA")
SELECT pg_catalog.obj_description(c.oid, 'pg_class')
FROM pg_catalog.pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
@@ -353,7 +353,7 @@ module ActiveRecord
end
def primary_keys(table_name) # :nodoc:
- query_values(<<-SQL.strip_heredoc, "SCHEMA")
+ query_values(<<~SQL, "SCHEMA")
SELECT a.attname
FROM (
SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
@@ -502,7 +502,7 @@ module ActiveRecord
def foreign_keys(table_name)
scope = quoted_scope(table_name)
- fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA")
+ fk_info = exec_query(<<~SQL, "SCHEMA")
SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid
FROM pg_constraint c
JOIN pg_class t1 ON c.conrelid = t1.oid
@@ -583,7 +583,7 @@ module ActiveRecord
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super, *order_columns].join(", ")
+ (order_columns << super).join(", ")
end
def update_table_definition(table_name, base) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index dc6287e32c..fdf6f75108 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -465,7 +465,7 @@ module ActiveRecord
register_class_with_limit m, "bit", OID::Bit
register_class_with_limit m, "varbit", OID::BitVarying
m.alias_type "timestamptz", "timestamp"
- m.register_type "date", Type::Date.new
+ m.register_type "date", OID::Date.new
m.register_type "money", OID::Money.new
m.register_type "bytea", OID::Bytea.new
@@ -837,6 +837,7 @@ module ActiveRecord
ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql)
ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql)
ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql)
+ ActiveRecord::Type.register(:date, OID::Date, adapter: :postgresql)
ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql)
ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql)
ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 1a3e6e4d09..23ecb24542 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -141,10 +141,7 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :name, :mapping, :subtype
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index efcbd44776..c2a180c939 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -120,13 +120,13 @@ module ActiveRecord
def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil)
@adapter = adapter
if table
- msg = <<-EOM.strip_heredoc
+ msg = +<<~EOM
Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`.
This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`.
To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`).
EOM
else
- msg = <<-EOM
+ msg = +<<~EOM
There is a mismatch between the foreign key and primary key column types.
Verify that the foreign key column type and the primary key of the associated table match types.
EOM
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index d9a75d9ad6..8f022ff7a7 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -874,6 +874,7 @@ module ActiveRecord
class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances
class_attribute :pre_loaded_fixtures, default: false
class_attribute :config, default: ActiveRecord::Base
+ class_attribute :lock_threads, default: true
end
module ClassMethods
@@ -973,7 +974,7 @@ module ActiveRecord
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
connection.begin_transaction joinable: false
- connection.pool.lock_thread = true
+ connection.pool.lock_thread = true if lock_threads
end
# When connections are established in the future, begin a transaction too
@@ -989,7 +990,7 @@ module ActiveRecord
if connection && !@fixture_connections.include?(connection)
connection.begin_transaction joinable: false
- connection.pool.lock_thread = true
+ connection.pool.lock_thread = true if lock_threads
@fixture_connections << connection
end
end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index f3fe610c09..77cfb36b25 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -104,21 +104,47 @@ module ActiveRecord
end
end
- # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
- # If you are using inheritance with ActiveRecord and don't want child classes
- # to utilize the implied STI table name of the parent class, this will need to be true.
- # For example, given the following:
+ # Set this to +true+ if this is an abstract class (see
+ # <tt>abstract_class?</tt>).
+ # If you are using inheritance with Active Record and don't want a class
+ # to be considered as part of the STI hierarchy, you must set this to
+ # true.
+ # +ApplicationRecord+, for example, is generated as an abstract class.
#
- # class SuperClass < ActiveRecord::Base
+ # Consider the following default behaviour:
+ #
+ # Shape = Class.new(ActiveRecord::Base)
+ # Polygon = Class.new(Shape)
+ # Square = Class.new(Polygon)
+ #
+ # Shape.table_name # => "shapes"
+ # Polygon.table_name # => "shapes"
+ # Square.table_name # => "shapes"
+ # Shape.create! # => #<Shape id: 1, type: nil>
+ # Polygon.create! # => #<Polygon id: 2, type: "Polygon">
+ # Square.create! # => #<Square id: 3, type: "Square">
+ #
+ # However, when using <tt>abstract_class</tt>, +Shape+ is omitted from
+ # the hierarchy:
+ #
+ # class Shape < ActiveRecord::Base
# self.abstract_class = true
# end
- # class Child < SuperClass
- # self.table_name = 'the_table_i_really_want'
- # end
- #
+ # Polygon = Class.new(Shape)
+ # Square = Class.new(Polygon)
#
- # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
+ # Shape.table_name # => nil
+ # Polygon.table_name # => "polygons"
+ # Square.table_name # => "polygons"
+ # Shape.create! # => NotImplementedError: Shape is an abstract class and cannot be instantiated.
+ # Polygon.create! # => #<Polygon id: 1, type: nil>
+ # Square.create! # => #<Square id: 2, type: "Square">
#
+ # Note that in the above example, to disallow the creation of a plain
+ # +Polygon+, you should use <tt>validates :type, presence: true</tt>,
+ # instead of setting it as an abstract class. This way, +Polygon+ will
+ # stay in the hierarchy, and Active Record will continue to correctly
+ # derive the table name.
attr_accessor :abstract_class
# Returns whether this class is an abstract class or not.
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
index 5a65edf27e..3626a13d7c 100644
--- a/activerecord/lib/active_record/internal_metadata.rb
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -17,7 +17,7 @@ module ActiveRecord
end
def []=(key, value)
- find_or_initialize_by(key: key).update_attributes!(value: value)
+ find_or_initialize_by(key: key).update!(value: value)
end
def [](key)
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index e1e24e2814..bf17f27e68 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -78,6 +78,7 @@ module ActiveRecord
end
def _update_record(attribute_names = self.attribute_names)
+ attribute_names &= self.class.column_names
return super unless locking_enabled?
return 0 if attribute_names.empty?
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 5322571820..663b3c590a 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -140,6 +140,7 @@ module ActiveRecord
class ConcurrentMigrationError < MigrationError #:nodoc:
DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze
+ RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze
def initialize(message = DEFAULT_MESSAGE)
super
@@ -1355,12 +1356,17 @@ module ActiveRecord
def with_advisory_lock
lock_id = generate_migrator_advisory_lock_id
- got_lock = Base.connection.get_advisory_lock(lock_id)
+ connection = Base.connection
+ got_lock = connection.get_advisory_lock(lock_id)
raise ConcurrentMigrationError unless got_lock
load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
yield
ensure
- Base.connection.release_advisory_lock(lock_id) if got_lock
+ if got_lock && !connection.release_advisory_lock(lock_id)
+ raise ConcurrentMigrationError.new(
+ ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
+ )
+ end
end
MIGRATOR_SALT = 2053462845
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 81ef4828f8..087632b10f 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -85,7 +85,7 @@ module ActiveRecord
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
- raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true)
+ raise IrreversibleMigration, <<~MSG unless respond_to?(method, true)
This migration uses #{command}, which is not automatically reversible.
To make the migration reversible you can either:
1. Define #up and #down methods in place of the #change method.
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index a45d011d75..4eecbd0629 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -418,6 +418,7 @@ module ActiveRecord
end
alias update_attributes update
+ deprecate :update_attributes
# Updates its receiver just like #update but calls #save! instead
# of +save+, so an exception is raised if the record is invalid and saving will fail.
@@ -431,6 +432,7 @@ module ActiveRecord
end
alias update_attributes! update!
+ deprecate :update_attributes!
# Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
@@ -702,6 +704,7 @@ module ActiveRecord
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def _update_record(attribute_names = self.attribute_names)
+ attribute_names &= self.class.column_names
attributes_values = arel_attributes_with_values_for_update(attribute_names)
if attributes_values.empty?
rows_affected = 0
@@ -719,6 +722,7 @@ module ActiveRecord
# Creates a record with values matching those of the instance attributes
# and returns its id.
def _create_record(attribute_names = self.attribute_names)
+ attribute_names &= self.class.column_names
attributes_values = arel_attributes_with_values_for_create(attribute_names)
new_id = self.class._insert_record(attributes_values)
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 8e23128333..c8e340712d 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -7,20 +7,20 @@ module ActiveRecord
# Enable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def cache(&block)
- if configurations.empty?
- yield
- else
+ if connected? || !configurations.empty?
connection.cache(&block)
+ else
+ yield
end
end
# Disable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def uncached(&block)
- if configurations.empty?
- yield
- else
+ if connected? || !configurations.empty?
connection.uncached(&block)
+ else
+ yield
end
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 3996d5661f..7c8f794910 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -5,7 +5,7 @@ module ActiveRecord
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all
delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
- delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all
delegate :find_by, :find_by!, to: :all
delegate :destroy_all, :delete_all, :update_all, to: :all
delegate :find_each, :find_in_batches, :in_batches, to: :all
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index 2ae733f657..309441a057 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -8,49 +8,44 @@ module ActiveRecord
module ControllerRuntime #:nodoc:
extend ActiveSupport::Concern
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_internal :db_runtime
-
- private
-
- def process_action(action, *args)
- # We also need to reset the runtime before each action
- # because of queries in middleware or in cases we are streaming
- # and it won't be cleaned up by the method below.
- ActiveRecord::LogSubscriber.reset_runtime
- super
+ module ClassMethods # :nodoc:
+ def log_process_action(payload)
+ messages, db_runtime = super, payload[:db_runtime]
+ messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
+ messages
+ end
end
- def cleanup_view_runtime
- if logger && logger.info? && ActiveRecord::Base.connected?
- db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime = (db_runtime || 0) + db_rt_before_render
- runtime = super
- db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime += db_rt_after_render
- runtime - db_rt_after_render
- else
+ private
+ attr_internal :db_runtime
+
+ def process_action(action, *args)
+ # We also need to reset the runtime before each action
+ # because of queries in middleware or in cases we are streaming
+ # and it won't be cleaned up by the method below.
+ ActiveRecord::LogSubscriber.reset_runtime
super
end
- end
- def append_info_to_payload(payload)
- super
- if ActiveRecord::Base.connected?
- payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ def cleanup_view_runtime
+ if logger && logger.info? && ActiveRecord::Base.connected?
+ db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime = (db_runtime || 0) + db_rt_before_render
+ runtime = super
+ db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime += db_rt_after_render
+ runtime - db_rt_after_render
+ else
+ super
+ end
end
- end
- module ClassMethods # :nodoc:
- def log_process_action(payload)
- messages, db_runtime = super, payload[:db_runtime]
- messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
- messages
+ def append_info_to_payload(payload)
+ super
+ if ActiveRecord::Base.connected?
+ payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 2e55713311..662a8bc720 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -229,7 +229,7 @@ db_namespace = namespace :db do
base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
Dir["#{base_dir}/**/*.yml"].each do |file|
- if data = YAML::load(ERB.new(IO.read(file)).result)
+ if data = YAML.load(ERB.new(IO.read(file)).result)
data.each_key do |key|
key_id = ActiveRecord::FixtureSet.identify(key)
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index e640d75d2f..42b451241d 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -193,7 +193,7 @@ module ActiveRecord
klass_scope = klass_join_scope(table, predicate_builder)
if type
- klass_scope.where!(type => foreign_klass.base_class.sti_name)
+ klass_scope.where!(type => foreign_klass.base_class.name)
end
scope_chain_items.inject(klass_scope, &:merge!)
@@ -416,6 +416,9 @@ module ActiveRecord
# Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
def compute_class(name)
+ if polymorphic?
+ raise ArgumentError, "Polymorphic association does not support to compute class."
+ end
active_record.send(:compute_type, name)
end
@@ -626,9 +629,6 @@ module ActiveRecord
# +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
- #
- # Note: klass will always be valid because when there's a NameError
- # from calling +klass+, +reflection+ will already be set to false.
def valid_inverse_reflection?(reflection)
reflection &&
klass <= reflection.active_record &&
@@ -732,6 +732,9 @@ module ActiveRecord
end
private
+ def can_find_inverse_of_automatically?(_)
+ !polymorphic? && super
+ end
def calculate_constructable(macro, options)
!polymorphic?
@@ -962,16 +965,14 @@ module ActiveRecord
collect_join_reflections(seed + [self])
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
- attr_reader :delegate_reflection
-
def actual_source_reflection # FIXME: this is a horrible name
source_reflection.actual_source_reflection
end
private
+ attr_reader :delegate_reflection
+
def collect_join_reflections(seed)
a = source_reflection.add_as_source seed
if options[:source_type]
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index d71c430045..736173ae1b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -142,23 +142,12 @@ module ActiveRecord
# failed due to validation errors it won't be persisted, you get what
# #create returns in such situation.
#
- # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if
# there are no results an INSERT is attempted. If there are other threads
# or processes there is a race condition between both calls and it could
# be the case that you end up with two similar records.
#
- # Whether that is a problem or not depends on the logic of the
- # application, but in the particular case in which rows have a UNIQUE
- # constraint an exception may be raised, just retry:
- #
- # begin
- # CreditAccount.transaction(requires_new: true) do
- # CreditAccount.find_or_create_by(user_id: user.id)
- # end
- # rescue ActiveRecord::RecordNotUnique
- # retry
- # end
- #
+ # If this might be a problem for your application, please see #create_or_find_by.
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
@@ -170,6 +159,47 @@ module ActiveRecord
find_by(attributes) || create!(attributes, &block)
end
+ # Attempts to create a record with the given attributes in a table that has a unique constraint
+ # on one or several of its columns. If a row already exists with one or several of these
+ # unique constraints, the exception such an insertion would normally raise is caught,
+ # and the existing record with those attributes is found using #find_by.
+ #
+ # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
+ # and the INSERT, as that method needs to first query the table, then attempt to insert a row
+ # if none is found.
+ #
+ # There are several drawbacks to #create_or_find_by, though:
+ #
+ # * The underlying table must have the relevant columns defined with unique constraints.
+ # * A unique constraint violation may be triggered by only one, or at least less than all,
+ # of the given attributes. This means that the subsequent #find_by may fail to find a
+ # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception,
+ # rather than a record with the given attributes.
+ # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by,
+ # we actually have another race condition between INSERT -> SELECT, which can be triggered
+ # if a DELETE between those two statements is run by another client. But for most applications,
+ # that's a significantly less likely condition to hit.
+ # * It relies on exception handling to handle control flow, which may be marginally slower.
+ #
+ # This method will return a record if all given attributes are covered by unique constraints
+ # (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted
+ # and failed due to validation errors it won't be persisted, you get what #create returns in
+ # such situation.
+ def create_or_find_by(attributes, &block)
+ transaction(requires_new: true) { create(attributes, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by!(attributes)
+ end
+
+ # Like #create_or_find_by, but calls
+ # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
+ # is raised if the created record is invalid.
+ def create_or_find_by!(attributes, &block)
+ transaction(requires_new: true) { create!(attributes, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by!(attributes)
+ end
+
# Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
# instead of {create}[rdoc-ref:Persistence::ClassMethods#create].
def find_or_initialize_by(attributes, &block)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index dba2f33dd4..f215c95f51 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -132,7 +132,7 @@ module ActiveRecord
if has_include?(column_name)
relation = apply_join_dependency
- if operation.to_s.downcase == "count" && !distinct_value
+ if operation.to_s.downcase == "count"
relation.distinct!
# PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
if (column_name == :all || column_name.nil?) && select_values.empty?
@@ -200,6 +200,24 @@ module ActiveRecord
end
end
+ # Pick the value(s) from the named column(s) in the current relation.
+ # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful
+ # when you have a relation that's already narrowed down to a single row.
+ #
+ # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also
+ # more efficient. The value is, again like with pluck, typecast by the column type.
+ #
+ # Person.where(id: 1).pick(:name)
+ # # SELECT people.name FROM people WHERE id = 1 LIMIT 1
+ # # => 'David'
+ #
+ # Person.where(id: 1).pick(:name, :email_address)
+ # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1
+ # # => [ 'David', 'david@loudthinking.com' ]
+ def pick(*column_names)
+ limit(1).pluck(*column_names).first
+ end
+
# Pluck all the ID's for the relation using the table's primary key
#
# Person.ids # SELECT people.id FROM people
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 4863befec8..4b16b49cdf 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -38,7 +38,7 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :encode_with, :length, :each, :uniq, :join,
+ delegate :to_xml, :encode_with, :length, :each, :join,
:[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
:to_sentence, :to_formatted_s, :as_json,
:shuffle, :split, :slice, :index, :rindex, to: :records
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index f3286846d2..7a0edcbc33 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -57,9 +57,6 @@ module ActiveRecord
end
protected
-
- attr_reader :table
-
def expand_from_hash(attributes)
return ["1=0"] if attributes.empty?
@@ -109,6 +106,7 @@ module ActiveRecord
end
private
+ attr_reader :table
def associated_predicate_builder(association_name)
self.class.new(table.associated_table(association_name))
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
index 2fd75c8958..64bf83e3c1 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -34,8 +34,7 @@ module ActiveRecord
array_predicates.inject(&:or)
end
- protected
-
+ private
attr_reader :predicate_builder
module NullPredicate # :nodoc:
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
index 28c7483c95..88cd71cf69 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
@@ -12,12 +12,9 @@ module ActiveRecord
[associated_table.association_join_foreign_key.to_s => ids]
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :associated_table, :value
- private
def ids
case value
when Relation
diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
index 112821135f..10c5c1a66a 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
@@ -11,8 +11,7 @@ module ActiveRecord
predicate_builder.build(attribute, value.id)
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
index 34db266f05..e8c9f60860 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
@@ -12,8 +12,7 @@ module ActiveRecord
attribute.eq(bind)
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
index e8e2f2c626..a5e9a0473e 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
@@ -17,12 +17,9 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :associated_table, :values
- private
def type_to_ids_mapping
default_hash = Hash.new { |hsh, key| hsh[key] = [] }
values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) }
diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
index 6d16579708..44bb2c7ab6 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
@@ -16,15 +16,16 @@ module ActiveRecord
def call(attribute, value)
begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin)
end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end)
- if value.begin.respond_to?(:infinite?) && value.begin.infinite?
- if value.end.respond_to?(:infinite?) && value.end.infinite?
+
+ if begin_bind.value.infinity?
+ if end_bind.value.infinity?
attribute.not_in([])
elsif value.exclude_end?
attribute.lt(end_bind)
else
attribute.lteq(end_bind)
end
- elsif value.end.respond_to?(:infinite?) && value.end.infinite?
+ elsif end_bind.value.infinity?
attribute.gteq(begin_bind)
elsif value.exclude_end?
attribute.gteq(begin_bind).and(attribute.lt(end_bind))
@@ -33,8 +34,7 @@ module ActiveRecord
end
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index 3532f28858..f64bd30d38 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -21,6 +21,23 @@ module ActiveRecord
!value_before_type_cast.is_a?(StatementCache::Substitute) &&
(value_before_type_cast.nil? || value_for_database.nil?)
end
+
+ def boundable?
+ return @_boundable if defined?(@_boundable)
+ nil?
+ @_boundable = true
+ rescue ::RangeError
+ @_boundable = false
+ end
+
+ def infinity?
+ _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database)
+ end
+
+ private
+ def _infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 86882c7ce7..3afa368575 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -231,6 +231,7 @@ module ActiveRecord
end
def _select!(*fields) # :nodoc:
+ fields.reject!(&:blank?)
fields.flatten!
fields.map! do |field|
klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index 92b720068c..c1b3eea9df 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -26,8 +26,7 @@ module ActiveRecord
WhereClause.new(parts)
end
- protected
-
+ private
attr_reader :klass, :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index 59acd63a0f..b41d3504fd 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -114,8 +114,7 @@ module ActiveRecord
end
end
- protected
-
+ private
attr_reader :query_builder, :bind_map, :klass
end
end
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index d3b9171159..b67479fb6a 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -73,10 +73,7 @@ module ActiveRecord
klass.reflect_on_aggregation(aggregation_name)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :klass, :arel_table, :association
end
end
diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb
new file mode 100644
index 0000000000..606a3b0fb5
--- /dev/null
+++ b/activerecord/lib/active_record/test_databases.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "active_support/testing/parallelization"
+
+module ActiveRecord
+ module TestDatabases # :nodoc:
+ ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
+ create_and_migrate(i, spec_name: Rails.env)
+ end
+
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do |i|
+ drop(i, spec_name: Rails.env)
+ end
+
+ def self.create_and_migrate(i, spec_name:)
+ old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
+
+ connection_spec = ActiveRecord::Base.configurations[spec_name]
+
+ connection_spec["database"] += "-#{i}"
+ ActiveRecord::Tasks::DatabaseTasks.create(connection_spec)
+ ActiveRecord::Base.establish_connection(connection_spec)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ ensure
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
+ ENV["VERBOSE"] = old
+ end
+
+ def self.drop(i, spec_name:)
+ old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
+ connection_spec = ActiveRecord::Base.configurations[spec_name]
+
+ ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec)
+ ensure
+ ENV["VERBOSE"] = old
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
index e7468aa542..b300fdfa05 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -52,8 +52,6 @@ module ActiveRecord
priority <=> other.priority
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
attr_reader :name, :block, :adapter, :override
@@ -114,13 +112,8 @@ module ActiveRecord
super | 4
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :options, :klass
-
private
+ attr_reader :options, :klass
def matches_options?(**kwargs)
options.all? do |key, value|
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index af4e4e37e2..7cf8181d8e 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -14,15 +14,10 @@ module ActiveRecord
connection.type_cast_from_column(column, value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :table_name
delegate :connection, to: :@klass
- private
-
def column_for(attribute_name)
if connection.schema_cache.data_source_exists?(table_name)
connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
index c5cfdba8b1..663cdadb03 100644
--- a/activerecord/lib/active_record/type_caster/map.rb
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -13,10 +13,7 @@ module ActiveRecord
type.serialize(value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :types
end
end
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index 856fcc5897..a07b00ef79 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -15,12 +15,8 @@ module ActiveRecord
migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb")
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :migration_action, :join_tables
-
private
+ attr_reader :migration_action, :join_tables
# Sets the default migration template that is being used for the generation of the migration.
# Depending on command line arguments, the migration template and the table name instance
diff --git a/activerecord/test/.gitignore b/activerecord/test/.gitignore
deleted file mode 100644
index a0ec5967dd..0000000000
--- a/activerecord/test/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/config.yml
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 0092146e09..4c41a68407 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -20,7 +20,7 @@ module ActiveRecord
b = Book.create(name: "my \x00 book")
b.reload
assert_equal "my \x00 book", b.name
- b.update_attributes(name: "my other \x00 book")
+ b.update(name: "my other \x00 book")
b.reload
assert_equal "my other \x00 book", b.name
end
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
index d18fb97e05..0719baaa23 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -25,25 +25,25 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
end
def test_columns_for_distinct_one_order
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
def test_columns_for_distinct_few_orders
- assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
def test_columns_for_distinct_with_case
assert_equal(
- "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0",
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id",
["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
)
end
def test_columns_for_distinct_blank_not_nil_orders
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
@@ -52,7 +52,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
def order.to_sql
"posts.created_at desc"
end
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", [order])
end
diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
index f921515c10..30455fbde5 100644
--- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
@@ -46,7 +46,7 @@ module ActiveRecord
Sample.transaction do
s1.lock!
barrier.wait
- s2.update_attributes value: 1
+ s2.update value: 1
end
end
@@ -54,7 +54,7 @@ module ActiveRecord
Sample.transaction do
s2.lock!
barrier.wait
- s1.update_attributes value: 2
+ s1.update value: 2
end
ensure
thread.join
diff --git a/activerecord/test/cases/adapters/postgresql/date_test.rb b/activerecord/test/cases/adapters/postgresql/date_test.rb
new file mode 100644
index 0000000000..a86abac2be
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/date_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+class PostgresqlDateTest < ActiveRecord::PostgreSQLTestCase
+ def test_load_infinity_and_beyond
+ topic = Topic.find_by_sql("SELECT 'infinity'::date AS last_read").first
+ assert topic.last_read.infinite?, "timestamp should be infinite"
+ assert_operator topic.last_read, :>, 0
+
+ topic = Topic.find_by_sql("SELECT '-infinity'::date AS last_read").first
+ assert topic.last_read.infinite?, "timestamp should be infinite"
+ assert_operator topic.last_read, :<, 0
+ end
+
+ def test_save_infinity_and_beyond
+ topic = Topic.create!(last_read: 1.0 / 0.0)
+ assert_equal(1.0 / 0.0, topic.last_read)
+
+ topic = Topic.create!(last_read: -1.0 / 0.0)
+ assert_equal(-1.0 / 0.0, topic.last_read)
+ end
+
+ def test_bc_date
+ date = Date.new(0) - 1.week
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+
+ def test_bc_date_leap_year
+ date = Time.utc(-4, 2, 29).to_date
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+
+ def test_bc_date_year_zero
+ date = Time.utc(0, 4, 7).to_date
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
index 0b18c0c9d7..5e56ce8427 100644
--- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -13,6 +13,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
@connection.create_table(:postgresql_infinities) do |t|
t.float :float
t.datetime :datetime
+ t.date :date
end
end
@@ -43,11 +44,25 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
end
test "type casting infinity on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: "infinity")
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+
record = PostgresqlInfinity.create!(datetime: Float::INFINITY)
record.reload
assert_equal Float::INFINITY, record.datetime
end
+ test "type casting infinity on a date column" do
+ record = PostgresqlInfinity.create!(date: "infinity")
+ record.reload
+ assert_equal Float::INFINITY, record.date
+
+ record = PostgresqlInfinity.create!(date: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.date
+ end
+
test "update_all with infinity on a datetime column" do
record = PostgresqlInfinity.create!
PostgresqlInfinity.update_all(datetime: Float::INFINITY)
@@ -68,4 +83,28 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
PostgresqlInfinity.reset_column_information
end
end
+
+ test "where clause with infinite range on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: Time.current)
+
+ string = PostgresqlInfinity.where(datetime: "-infinity".."infinity")
+ assert_equal record, string.take
+
+ infinity = PostgresqlInfinity.where(datetime: -::Float::INFINITY..::Float::INFINITY)
+ assert_equal record, infinity.take
+
+ assert_equal infinity.to_sql, string.to_sql
+ end
+
+ test "where clause with infinite range on a date column" do
+ record = PostgresqlInfinity.create!(date: Date.current)
+
+ string = PostgresqlInfinity.where(date: "-infinity".."infinity")
+ assert_equal record, string.take
+
+ infinity = PostgresqlInfinity.where(date: -::Float::INFINITY..::Float::INFINITY)
+ assert_equal record, infinity.take
+
+ assert_equal infinity.to_sql, string.to_sql
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 1951230c8a..cbb6cd42b5 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -263,25 +263,25 @@ module ActiveRecord
end
def test_columns_for_distinct_one_order
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
def test_columns_for_distinct_few_orders
- assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
def test_columns_for_distinct_with_case
assert_equal(
- "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0",
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id",
["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
)
end
def test_columns_for_distinct_blank_not_nil_orders
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
@@ -290,23 +290,23 @@ module ActiveRecord
def order.to_sql
"posts.created_at desc"
end
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", [order])
end
def test_columns_for_distinct_with_nulls
- assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
- assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
+ assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
+ assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
end
def test_columns_for_distinct_without_order_specifiers
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id"])
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"])
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"])
end
diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
index 9821b103df..8e952a9408 100644
--- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
@@ -76,7 +76,7 @@ module ActiveRecord
Sample.transaction do
s1.lock!
barrier.wait
- s2.update_attributes value: 1
+ s2.update value: 1
end
end
@@ -84,7 +84,7 @@ module ActiveRecord
Sample.transaction do
s2.lock!
barrier.wait
- s1.update_attributes value: 2
+ s1.update value: 2
end
ensure
thread.join
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
index 8754889143..4776e11128 100644
--- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -11,21 +11,33 @@ module Namespaced
end
end
-class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
+module PolymorphicFullStiClassNamesSharedTest
def setup
+ @old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+
post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1)
@tagging = Tagging.create(taggable: post)
- @old = ActiveRecord::Base.store_full_sti_class
end
def teardown
- ActiveRecord::Base.store_full_sti_class = @old
+ ActiveRecord::Base.store_full_sti_class = @old_store_full_sti_class
+ end
+
+ def test_class_names
+ ActiveRecord::Base.store_full_sti_class = false
+ post = Namespaced::Post.find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = true
+ post = Namespaced::Post.find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
end
def test_class_names_with_includes
ActiveRecord::Base.store_full_sti_class = false
post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
- assert_nil post.tagging
+ assert_equal @tagging, post.tagging
ActiveRecord::Base.store_full_sti_class = true
post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
@@ -35,10 +47,28 @@ class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
def test_class_names_with_eager_load
ActiveRecord::Base.store_full_sti_class = false
post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
- assert_nil post.tagging
+ assert_equal @tagging, post.tagging
ActiveRecord::Base.store_full_sti_class = true
post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
assert_equal @tagging, post.tagging
end
end
+
+class PolymorphicFullStiClassNamesTest < ActiveRecord::TestCase
+ include PolymorphicFullStiClassNamesSharedTest
+
+ private
+ def store_full_sti_class
+ true
+ end
+end
+
+class PolymorphicNonFullStiClassNamesTest < ActiveRecord::TestCase
+ include PolymorphicFullStiClassNamesSharedTest
+
+ private
+ def store_full_sti_class
+ false
+ end
+end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index d9e05cf7e2..5f771fe85f 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -662,7 +662,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_includes developer.sym_special_projects, sp
end
- def test_update_attributes_after_push_without_duplicate_join_table_rows
+ def test_update_columns_after_push_without_duplicate_join_table_rows
developer = Developer.new("name" => "Kano")
project = SpecialProject.create("name" => "Special Project")
assert developer.save
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 9ee42cef0f..00821f2319 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1211,20 +1211,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_calling_update_attributes_on_id_changes_the_counter_cache
+ def test_calling_update_on_id_changes_the_counter_cache
topic = Topic.order("id ASC").first
original_count = topic.replies.to_a.size
assert_equal original_count, topic.replies_count
first_reply = topic.replies.first
- first_reply.update_attributes(parent_id: nil)
+ first_reply.update(parent_id: nil)
assert_equal original_count - 1, topic.reload.replies_count
- first_reply.update_attributes(parent_id: topic.id)
+ first_reply.update(parent_id: topic.id)
assert_equal original_count, topic.reload.replies_count
end
- def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache
+ def test_calling_update_changing_ids_doesnt_change_counter_cache
topic1 = Topic.find(1)
topic2 = Topic.find(3)
original_count1 = topic1.replies.to_a.size
@@ -1233,11 +1233,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
reply1 = topic1.replies.first
reply2 = topic2.replies.first
- reply1.update_attributes(parent_id: topic2.id)
+ reply1.update(parent_id: topic2.id)
assert_equal original_count1 - 1, topic1.reload.replies_count
assert_equal original_count2 + 1, topic2.reload.replies_count
- reply2.update_attributes(parent_id: topic1.id)
+ reply2.update(parent_id: topic1.id)
assert_equal original_count1, topic1.reload.replies_count
assert_equal original_count2, topic2.reload.replies_count
end
@@ -1842,6 +1842,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
end
+ def test_associations_order_should_be_priority_over_throughs_order
+ david = authors(:david)
+ expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1]
+ assert_equal expected, david.comments_desc.map(&:id)
+ assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id)
+ end
+
def test_dynamic_find_should_respect_association_order_for_through
assert_equal Comment.find(10), authors(:david).comments_desc.where("comments.type = 'SpecialComment'").first
assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type("SpecialComment")
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 25118b26f5..3b3d4037b9 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -46,6 +46,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create person_id: 0, post_id: 0
end
+ def test_marshal_dump
+ preloaded = Post.includes(:first_blue_tags).first
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
def test_preload_sti_rhs_class
developers = Developer.includes(:firms).all.to_a
assert_no_queries do
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index bad1bcdb67..896bf574f4 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -190,6 +190,16 @@ class InverseAssociationTests < ActiveRecord::TestCase
assert_nil belongs_to_ref.inverse_of
end
+ def test_polymorphic_associations_dont_attempt_to_find_inverse_of
+ belongs_to_ref = Sponsor.reflect_on_association(:sponsor)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+
+ belongs_to_ref = Face.reflect_on_association(:human)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+ end
+
def test_this_inverse_stuff
firm = Firm.create!(name: "Adequate Holdings")
Project.create!(name: "Project 1", firm: firm)
diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb
index 7b5c394177..0e54e8c1b0 100644
--- a/activerecord/test/cases/associations/left_outer_join_association_test.rb
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -5,6 +5,7 @@ require "models/post"
require "models/comment"
require "models/author"
require "models/essay"
+require "models/category"
require "models/categorization"
require "models/person"
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 09684520d1..b8243d148a 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -50,7 +50,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
}
u = person.create!(first_name: "cool")
- u.update_attributes!(first_name: "nah") # still valid because validation only applies on 'create'
+ u.update!(first_name: "nah") # still valid because validation only applies on 'create'
assert_predicate reference.create!(person: u), :persisted?
end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 983a3d366a..7dfb05a6a5 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1501,7 +1501,7 @@ class BasicsTest < ActiveRecord::TestCase
query = Developer.from("developers").to_sql
quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}"
- assert_match(/SELECT #{quoted_id}.* FROM developers/, query)
+ assert_match(/SELECT #{Regexp.escape(quoted_id)}.* FROM developers/, query)
end
test "using table name qualified column names unless having SELECT list explicitly" do
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index ad701fb200..c8163901c6 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -508,7 +508,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches
Post.update_all(author_id: 0)
person = Post.last
- person.update_attributes(author_id: 1)
+ person.update(author_id: 1)
Post.in_batches(of: 2) do |batch|
batch.where("author_id >= 1").update_all("author_id = author_id + 1")
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 82b15e565b..74228b2796 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -242,6 +242,12 @@ class CalculationsTest < ActiveRecord::TestCase
assert_queries(1) { assert_equal 11, posts.count(:all) }
end
+ def test_count_with_eager_loading_and_custom_order_and_distinct
+ posts = Post.includes(:comments).order("comments.id").distinct
+ assert_queries(1) { assert_equal 11, posts.count }
+ assert_queries(1) { assert_equal 11, posts.count(:all) }
+ end
+
def test_distinct_count_all_with_custom_select_and_order
accounts = Account.distinct.select("credit_limit % 10").order(Arel.sql("credit_limit % 10"))
assert_queries(1) { assert_equal 3, accounts.count(:all) }
@@ -793,6 +799,18 @@ class CalculationsTest < ActiveRecord::TestCase
end
end
+ def test_pick_one
+ assert_equal "The First Topic", Topic.order(:id).pick(:heading)
+ assert_nil Topic.none.pick(:heading)
+ assert_nil Topic.where("1=0").pick(:heading)
+ end
+
+ def test_pick_two
+ assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address)
+ assert_nil Topic.none.pick(:author_name, :author_email_address)
+ assert_nil Topic.where("1=0").pick(:author_name, :author_email_address)
+ end
+
def test_grouped_calculation_with_polymorphic_relation
part = ShipPart.create!(name: "has trinket")
part.trinkets.create!
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
index 356afdbd2b..6e7ae2efb4 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -4,7 +4,6 @@ require "cases/helper"
require "models/person"
require "models/topic"
require "pp"
-require "active_support/core_ext/string/strip"
class NonExistentTable < ActiveRecord::Base; end
@@ -39,26 +38,26 @@ class CoreTest < ActiveRecord::TestCase
topic = Topic.new
actual = "".dup
PP.pp(topic, StringIO.new(actual))
- expected = <<-PRETTY.strip_heredoc
- #<Topic:0xXXXXXX
- id: nil,
- title: nil,
- author_name: nil,
- author_email_address: "test@test.com",
- written_on: nil,
- bonus_time: nil,
- last_read: nil,
- content: nil,
- important: nil,
- approved: true,
- replies_count: 0,
- unique_replies_count: 0,
- parent_id: nil,
- parent_title: nil,
- type: nil,
- group: nil,
- created_at: nil,
- updated_at: nil>
+ expected = <<~PRETTY
+ #<Topic:0xXXXXXX
+ id: nil,
+ title: nil,
+ author_name: nil,
+ author_email_address: "test@test.com",
+ written_on: nil,
+ bonus_time: nil,
+ last_read: nil,
+ content: nil,
+ important: nil,
+ approved: true,
+ replies_count: 0,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: nil,
+ updated_at: nil>
PRETTY
assert actual.start_with?(expected.split("XXXXXX").first)
assert actual.end_with?(expected.split("XXXXXX").last)
@@ -68,26 +67,26 @@ class CoreTest < ActiveRecord::TestCase
topic = topics(:first)
actual = "".dup
PP.pp(topic, StringIO.new(actual))
- expected = <<-PRETTY.strip_heredoc
- #<Topic:0x\\w+
- id: 1,
- title: "The First Topic",
- author_name: "David",
- author_email_address: "david@loudthinking.com",
- written_on: 2003-07-16 14:28:11 UTC,
- bonus_time: 2000-01-01 14:28:00 UTC,
- last_read: Thu, 15 Apr 2004,
- content: "Have a nice day",
- important: nil,
- approved: false,
- replies_count: 1,
- unique_replies_count: 0,
- parent_id: nil,
- parent_title: nil,
- type: nil,
- group: nil,
- created_at: [^,]+,
- updated_at: [^,>]+>
+ expected = <<~PRETTY
+ #<Topic:0x\\w+
+ id: 1,
+ title: "The First Topic",
+ author_name: "David",
+ author_email_address: "david@loudthinking.com",
+ written_on: 2003-07-16 14:28:11 UTC,
+ bonus_time: 2000-01-01 14:28:00 UTC,
+ last_read: Thu, 15 Apr 2004,
+ content: "Have a nice day",
+ important: nil,
+ approved: false,
+ replies_count: 1,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: [^,]+,
+ updated_at: [^,>]+>
PRETTY
assert_match(/\A#{expected}\z/, actual)
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 14c4e3cbd4..2f1434b2bc 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -736,6 +736,24 @@ class DirtyTest < ActiveRecord::TestCase
assert record.save
end
+ test "virtual attributes are not written with partial_writes off" do
+ with_partial_writes(ActiveRecord::Base, false) do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "people"
+ attribute :non_persisted_attribute, :string
+ end
+
+ record = klass.new(first_name: "Sean")
+ record.non_persisted_attribute_will_change!
+
+ assert record.save
+
+ record.non_persisted_attribute_will_change!
+
+ assert record.save
+ end
+ end
+
test "mutating and then assigning doesn't remove the change" do
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index 17654027a9..a0e75f4e89 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -2,7 +2,6 @@
require "cases/helper"
require "models/car"
-require "active_support/core_ext/string/strip"
if ActiveRecord::Base.connection.supports_explain?
class ExplainTest < ActiveRecord::TestCase
@@ -53,7 +52,7 @@ if ActiveRecord::Base.connection.supports_explain?
queries = sqls.zip(binds)
stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do
- expected = <<-SQL.strip_heredoc
+ expected = <<~SQL
EXPLAIN for: #{sqls[0]} [["wadus", 1]]
query plan foo
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index b413212e26..d85910d9c6 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -812,6 +812,15 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [1, 2, 6, 7, 8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort
end
+ def test_find_on_hash_conditions_with_open_ended_range
+ assert_equal [1, 2, 3], Comment.where(id: Float::INFINITY..3).to_a.map(&:id).sort
+ end
+
+ def test_find_on_hash_conditions_with_numeric_range_for_string
+ topic = Topic.create!(title: "12 Factor App")
+ assert_equal [topic], Topic.where(title: 10..2).to_a
+ end
+
def test_find_on_multiple_hash_conditions
assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1)
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
@@ -1180,6 +1189,11 @@ class FinderTest < ActiveRecord::TestCase
order("author_addresses_authors.id DESC").limit(3).to_a.size
end
+ def test_find_with_eager_loading_collection_and_ordering_by_collection_primary_key
+ assert_equal Post.first, Post.eager_load(comments: :ratings).
+ order("posts.id, ratings.id, comments.id").first
+ end
+
def test_find_with_nil_inside_set_passed_for_one_attribute
client_of = Company.
where(client_of: [2, 1, nil],
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index c88b90c8eb..184b750161 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -86,7 +86,7 @@ class FixturesTest < ActiveRecord::TestCase
create_fixtures("bulbs", "authors", "computers")
- expected_sql = <<-EOS.strip_heredoc.chop
+ expected_sql = <<~EOS.chop
INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("bulbs")} .*
INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("authors")} .*
INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("computers")} .*
@@ -1186,7 +1186,7 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
end
def test_table_name_is_defined_in_the_model
- assert_equal "randomly_named_table2", ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name
+ assert_equal "randomly_named_table2", ActiveRecord::FixtureSet.all_loaded_fixtures["admin/randomly_named_a9"].table_name
assert_equal "randomly_named_table2", Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name
end
end
diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb
index c60a276850..9b79803503 100644
--- a/activerecord/test/cases/json_shared_test_cases.rb
+++ b/activerecord/test/cases/json_shared_test_cases.rb
@@ -101,7 +101,7 @@ module JSONSharedTestCases
x = klass.where(payload: nil).first
assert_nil(x)
- json.update_attributes(payload: nil)
+ json.update(payload: nil)
x = klass.where(payload: nil).first
assert_equal(json.reload, x)
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index bb76137ef5..02be0aa9e3 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -415,7 +415,9 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 1, car.lock_version
previously_car_updated_at = car.updated_at
- car.wheels.first.update(size: 42)
+ travel(1.day) do
+ car.wheels.first.update(size: 42)
+ end
assert_equal 1, car.reload.wheels_count
assert_not_equal previously_car_updated_at, car.updated_at
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 8514ccd55b..a9deb92585 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -88,7 +88,7 @@ module ActiveRecord
assert connection.table_exists? :felines
assert_not connection.table_exists? :cats
- primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0]
+ primary_key_name = connection.select_values(<<~SQL, "SCHEMA")[0]
SELECT c.relname
FROM pg_class c
JOIN pg_index i
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index a4b8664a85..c241ddc807 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -693,6 +693,25 @@ class MigrationTest < ActiveRecord::TestCase
assert_no_column Person, :last_name,
"without an advisory lock, the Migrator should not make any changes, but it did."
end
+
+ def test_with_advisory_lock_raises_the_right_error_when_it_fails_to_release_lock
+ migration = Class.new(ActiveRecord::Migration::Current).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ e = assert_raises(ActiveRecord::ConcurrentMigrationError) do
+ silence_stream($stderr) do
+ migrator.send(:with_advisory_lock) do
+ ActiveRecord::Base.connection.release_advisory_lock(lock_id)
+ end
+ end
+ end
+
+ assert_match(
+ /#{ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE}/,
+ e.message
+ )
+ end
end
private
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index d4a4cd564a..e4a65a48ca 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -525,7 +525,7 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_does_not_run_sql_if_record_has_not_changed
topic = Topic.create(title: "Another New Topic")
assert_queries(0) { assert topic.update(title: "Another New Topic") }
- assert_queries(0) { assert topic.update_attributes(title: "Another New Topic") }
+ assert_queries(0) { assert topic.update(title: "Another New Topic") }
end
def test_delete
@@ -934,42 +934,33 @@ class PersistenceTest < ActiveRecord::TestCase
topic.reload
assert_not_predicate topic, :approved?
assert_equal "The First Topic", topic.title
- end
-
- def test_update_attributes
- topic = Topic.find(1)
- assert_not_predicate topic, :approved?
- assert_equal "The First Topic", topic.title
-
- topic.update_attributes("approved" => true, "title" => "The First Topic Updated")
- topic.reload
- assert_predicate topic, :approved?
- assert_equal "The First Topic Updated", topic.title
-
- topic.update_attributes(approved: false, title: "The First Topic")
- topic.reload
- assert_not_predicate topic, :approved?
- assert_equal "The First Topic", topic.title
error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
- topic.update_attributes(id: 3, title: "Hm is it possible?")
+ topic.update(id: 3, title: "Hm is it possible?")
end
assert_not_nil error.cause
assert_not_equal "Hm is it possible?", Topic.find(3).title
- topic.update_attributes(id: 1234)
+ topic.update(id: 1234)
assert_nothing_raised { topic.reload }
assert_equal topic.title, Topic.find(1234).title
end
- def test_update_attributes_parameters
+ def test_update_attributes
+ topic = Topic.find(1)
+ assert_deprecated do
+ topic.update_attributes("title" => "The First Topic Updated")
+ end
+ end
+
+ def test_update_parameters
topic = Topic.find(1)
assert_nothing_raised do
- topic.update_attributes({})
+ topic.update({})
end
assert_raises(ArgumentError) do
- topic.update_attributes(nil)
+ topic.update(nil)
end
end
@@ -995,24 +986,10 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_attributes!
- Reply.validates_presence_of(:title)
reply = Reply.find(2)
- assert_equal "The Second Topic of the day", reply.title
- assert_equal "Have a nice day", reply.content
-
- reply.update_attributes!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening")
- reply.reload
- assert_equal "The Second Topic of the day updated", reply.title
- assert_equal "Have a nice evening", reply.content
-
- reply.update_attributes!(title: "The Second Topic of the day", content: "Have a nice day")
- reply.reload
- assert_equal "The Second Topic of the day", reply.title
- assert_equal "Have a nice day", reply.content
-
- assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") }
- ensure
- Reply.clear_validators!
+ assert_deprecated do
+ reply.update_attributes!("title" => "The Second Topic of the day updated")
+ end
end
def test_destroyed_returns_boolean
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 8d37f5ea95..d635a47c0e 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -320,6 +320,17 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_cache_is_available_when_connection_is_connected
+ conf = ActiveRecord::Base.configurations
+
+ ActiveRecord::Base.configurations = {}
+ Task.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+ ensure
+ ActiveRecord::Base.configurations = conf
+ end
+
def test_cache_is_available_when_using_a_not_connected_connection
skip "In-Memory DB can't test for using a not connected connection" if in_memory_db?
with_temporary_connection_pool do
@@ -366,7 +377,7 @@ class QueryCacheTest < ActiveRecord::TestCase
post = Post.first
Post.transaction do
- post.update_attributes(title: "rollback")
+ post.update(title: "rollback")
assert_equal 1, Post.where(title: "rollback").to_a.count
raise ActiveRecord::Rollback
end
@@ -379,7 +390,7 @@ class QueryCacheTest < ActiveRecord::TestCase
begin
Post.transaction do
- post.update_attributes(title: "rollback")
+ post.update(title: "rollback")
assert_equal 1, Post.where(title: "rollback").to_a.count
raise "broken"
end
diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb
new file mode 100644
index 0000000000..0577e6bfdb
--- /dev/null
+++ b/activerecord/test/cases/relation/select_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+module ActiveRecord
+ class SelectTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_select_with_nil_agrument
+ expected = Post.select(:title).to_sql
+ assert_equal expected, Post.select(nil).select(:title).to_sql
+ end
+ end
+end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index bc7c54dbe0..cf65789b97 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -22,6 +22,7 @@ require "models/reader"
require "models/category"
require "models/categorization"
require "models/edge"
+require "models/subscriber"
class RelationTest < ActiveRecord::TestCase
fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans
@@ -975,6 +976,12 @@ class RelationTest < ActiveRecord::TestCase
assert_queries(1) { assert_equal 11, posts.load.size }
end
+ def test_size_with_eager_loading_and_custom_order_and_distinct
+ posts = Post.includes(:comments).order("comments.id").distinct
+ assert_queries(1) { assert_equal 11, posts.size }
+ assert_queries(1) { assert_equal 11, posts.load.size }
+ end
+
def test_update_all_with_scope
tag = Tag.first
Post.tagged_with(tag.id).update_all title: "rofl"
@@ -1349,6 +1356,34 @@ class RelationTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") }
end
+ def test_create_or_find_by
+ assert_nil Subscriber.find_by(nick: "bob")
+
+ subscriber = Subscriber.create!(nick: "bob")
+
+ assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob")
+ assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat")
+ end
+
+ def test_create_or_find_by_with_non_unique_attributes
+ Subscriber.create!(nick: "bob", name: "the builder")
+
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Subscriber.create_or_find_by(nick: "bob", name: "the cat")
+ end
+ end
+
+ def test_create_or_find_by_within_transaction
+ assert_nil Subscriber.find_by(nick: "bob")
+
+ subscriber = Subscriber.create!(nick: "bob")
+
+ Subscriber.transaction do
+ assert_equal subscriber, Subscriber.create_or_find_by(nick: "bob")
+ assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat")
+ end
+ end
+
def test_find_or_initialize_by
assert_nil Bird.find_by(name: "bob")
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
index 73422a31cd..62cd89041a 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -56,7 +56,7 @@ class LengthValidationTest < ActiveRecord::TestCase
assert owner.save
pet_count = Pet.count
- assert_not owner.update_attributes pets_attributes: [ { _destroy: 1, id: pet.id } ]
+ assert_not owner.update pets_attributes: [ { _destroy: 1, id: pet.id } ]
assert_not_predicate owner, :valid?
assert_predicate owner.errors[:pets], :any?
assert_equal pet_count, Pet.count
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 1fa94a5b75..941aed5402 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -62,7 +62,7 @@ class TopicWithAfterCreate < Topic
after_create :set_author
def set_author
- update_attributes!(author_name: "#{title} #{id}")
+ update!(author_name: "#{title} #{id}")
end
end
diff --git a/activerecord/test/fixtures/.gitignore b/activerecord/test/fixtures/.gitignore
deleted file mode 100644
index 885029a512..0000000000
--- a/activerecord/test/fixtures/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.sqlite* \ No newline at end of file
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 27da886e1c..bd12cdf7ef 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -8,6 +8,7 @@ class Author < ActiveRecord::Base
has_many :posts_with_comments, -> { includes(:comments) }, class_name: "Post"
has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, class_name: "Post"
has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, class_name: "Post"
+ has_many :posts_sorted_by_id, -> { order(:id) }, class_name: "Post"
has_many :posts_sorted_by_id_limited, -> { order("posts.id").limit(1) }, class_name: "Post"
has_many :posts_with_categories, -> { includes(:categories) }, class_name: "Post"
has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, class_name: "Post"
@@ -40,7 +41,7 @@ class Author < ActiveRecord::Base
-> { where(title: "Welcome to the weblog").where(Post.arel_table[:comments_count].gt(0)) },
class_name: "Post"
- has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts, source: :comments
+ has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts_sorted_by_id, source: :comments
has_many :unordered_comments, -> { unscope(:order).distinct }, through: :posts_sorted_by_id_limited, source: :comments
has_many :funky_comments, through: :posts, source: :comments
has_many :ordered_uniq_comments, -> { distinct.order("comments.id") }, through: :posts, source: :comments
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index 5ab433f2d9..f0f0576709 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -76,7 +76,7 @@ class CommentThatAutomaticallyAltersPostBody < Comment
belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id
after_save do |comment|
- comment.post.update_attributes(body: "Automatically altered")
+ comment.post.update(body: "Automatically altered")
end
end
@@ -87,6 +87,6 @@ end
class CommentWithAfterCreateUpdate < Comment
after_create do
- update_attributes(body: "bar")
+ update(body: "bar")
end
end
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index 948435136d..e900fd40fb 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -2,6 +2,7 @@
class Face < ActiveRecord::Base
belongs_to :man, inverse_of: :face
+ belongs_to :human, polymorphic: true
belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face
# Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly`
belongs_to :poly_man_without_inverse, polymorphic: true
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index 3acd89a48e..e26920e951 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -11,3 +11,6 @@ class Man < ActiveRecord::Base
has_many :secret_interests, class_name: "Interest", inverse_of: :secret_man
has_one :mixed_case_monkey
end
+
+class Human < Man
+end
diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb
index f190860fd1..18ff103ffe 100644
--- a/activerecord/test/models/sponsor.rb
+++ b/activerecord/test/models/sponsor.rb
@@ -3,6 +3,7 @@
class Sponsor < ActiveRecord::Base
belongs_to :sponsor_club, class_name: "Club", foreign_key: "club_id"
belongs_to :sponsorable, polymorphic: true
+ belongs_to :sponsor, polymorphic: true
belongs_to :thing, polymorphic: true, foreign_type: :sponsorable_type, foreign_key: :sponsorable_id
belongs_to :sponsorable_with_conditions, -> { where name: "Ernie" }, polymorphic: true,
foreign_type: "sponsorable_type", foreign_key: "sponsorable_id"
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 7d008eecd5..3f4fe16678 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -814,6 +814,7 @@ ActiveRecord::Schema.define do
create_table :sponsors, force: true do |t|
t.integer :club_id
t.references :sponsorable, polymorphic: true, index: false
+ t.references :sponsor, polymorphic: true, index: false
end
create_table :string_key_objects, id: false, force: true do |t|
@@ -951,6 +952,7 @@ ActiveRecord::Schema.define do
t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
+ t.references :human, polymorphic: true, index: false
end
create_table :interests, force: true do |t|
diff --git a/activestorage/.gitignore b/activestorage/.gitignore
index 29bdcc4468..3e78878ffc 100644
--- a/activestorage/.gitignore
+++ b/activestorage/.gitignore
@@ -1,7 +1,6 @@
-.byebug_history
-node_modules
-src
-test/dummy/db/*.sqlite3
-test/dummy/db/*.sqlite3-journal
-test/dummy/log/*.log
-test/dummy/tmp/
+/src/
+/test/dummy/db/*.sqlite3
+/test/dummy/db/*.sqlite3-journal
+/test/dummy/log/*.log
+/test/dummy/tmp/
+/test/service/configurations.yml
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
index 682e35e0a8..2f8b65766d 100644
--- a/activestorage/CHANGELOG.md
+++ b/activestorage/CHANGELOG.md
@@ -1,8 +1,8 @@
-* Add source code to published npm package
+## Rails 6.0.0.alpha (Unreleased) ##
- This allows activestorage users to depend on the javascript source code
- rather than the compiled code, which can produce smaller javascript bundles.
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
- *Richard Macklin*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activestorage/CHANGELOG.md) for previous changes.
diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec
index d135324700..df7801a671 100644
--- a/activestorage/activestorage.gemspec
+++ b/activestorage/activestorage.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Local and cloud file storage framework."
s.description = "Attach cloud and local files in Rails applications."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js
index a2f06c43a3..661eb2e277 100644
--- a/activestorage/app/assets/javascripts/activestorage.js
+++ b/activestorage/app/assets/javascripts/activestorage.js
@@ -1 +1 @@
-!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=t.disabled,i=r.bubbles,a=r.cancelable,u=r.detail,o=document.createEvent("Event");o.initEvent(e,i||!0,a||!0),o.detail=u||{};try{t.disabled=!1,t.dispatchEvent(o)}finally{t.disabled=n}return o}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style="display:none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){if(this.status>=200&&this.status<300){var e=this.response,r=e.direct_upload;delete e.direct_upload,this.attributes=e,this.directUploadData=r,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}},{key:"status",get:function(){return this.xhr.status}},{key:"response",get:function(){var t=this.xhr,e=t.responseType,r=t.response;return"json"==e?r:JSON.parse(r)}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file)}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])}); \ No newline at end of file
+!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=t.disabled,i=r.bubbles,a=r.cancelable,u=r.detail,o=document.createEvent("Event");o.initEvent(e,i||!0,a||!0),o.detail=u||{};try{t.disabled=!1,t.dispatchEvent(o)}finally{t.disabled=n}return o}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style="display:none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){if(this.status>=200&&this.status<300){var e=this.response,r=e.direct_upload;delete e.direct_upload,this.attributes=e,this.directUploadData=r,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}},{key:"status",get:function(){return this.xhr.status}},{key:"response",get:function(){var t=this.xhr,e=t.responseType,r=t.response;return"json"==e?r:JSON.parse(r)}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file.slice())}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])}); \ No newline at end of file
diff --git a/activestorage/app/javascript/activestorage/blob_upload.js b/activestorage/app/javascript/activestorage/blob_upload.js
index 180a7415e7..277cc8ff8e 100644
--- a/activestorage/app/javascript/activestorage/blob_upload.js
+++ b/activestorage/app/javascript/activestorage/blob_upload.js
@@ -17,7 +17,7 @@ export class BlobUpload {
create(callback) {
this.callback = callback
- this.xhr.send(this.file)
+ this.xhr.send(this.file.slice())
}
requestDidLoad(event) {
diff --git a/activestorage/app/models/active_storage/blob.rb b/activestorage/app/models/active_storage/blob.rb
index 892a833fae..0cd4ad8128 100644
--- a/activestorage/app/models/active_storage/blob.rb
+++ b/activestorage/app/models/active_storage/blob.rb
@@ -14,7 +14,13 @@
# update a blob's metadata on a subsequent pass, but you should not update the key or change the uploaded file.
# If you need to create a derivative or otherwise change the blob, simply create a new blob and purge the old one.
class ActiveStorage::Blob < ActiveRecord::Base
- include Analyzable, Identifiable, Representable
+ require_dependency "active_storage/blob/analyzable"
+ require_dependency "active_storage/blob/identifiable"
+ require_dependency "active_storage/blob/representable"
+
+ include Analyzable
+ include Identifiable
+ include Representable
self.table_name = "active_storage_blobs"
@@ -25,6 +31,8 @@ class ActiveStorage::Blob < ActiveRecord::Base
has_many :attachments
+ scope :unattached, -> { left_joins(:attachments).where(ActiveStorage::Attachment.table_name => { blob_id: nil }) }
+
class << self
# You can used the signed ID of a blob to refer to it on the client side without fear of tampering.
# This is particularly helpful for direct uploads where the client-side needs to refer to the blob
@@ -109,7 +117,9 @@ class ActiveStorage::Blob < ActiveRecord::Base
# with users. Instead, the +service_url+ should only be exposed as a redirect from a stable, possibly authenticated URL.
# Hiding the +service_url+ behind a redirect also gives you the power to change services without updating all URLs. And
# it allows permanent URLs that redirect to the +service_url+ to be cached in the view.
- def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: self.filename, **options)
+ def service_url(expires_in: service.url_expires_in, disposition: :inline, filename: nil, **options)
+ filename = ActiveStorage::Filename.wrap(filename || self.filename)
+
service.url key, expires_in: expires_in, filename: filename, content_type: content_type,
disposition: forcibly_serve_as_binary? ? :attachment : disposition, **options
end
@@ -192,4 +202,6 @@ class ActiveStorage::Blob < ActiveRecord::Base
def forcibly_serve_as_binary?
ActiveStorage.content_types_to_serve_as_binary.include?(content_type)
end
+
+ ActiveSupport.run_load_hooks(:active_storage_blob, self)
end
diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb
index 40ca84ac70..dbe03cfa6c 100644
--- a/activestorage/app/models/active_storage/blob/identifiable.rb
+++ b/activestorage/app/models/active_storage/blob/identifiable.rb
@@ -2,10 +2,15 @@
module ActiveStorage::Blob::Identifiable
def identify
- ActiveStorage::Identification.new(self).apply
+ update!(content_type: identification.content_type, identified: true) unless identified?
end
def identified?
identified
end
+
+ private
+ def identification
+ ActiveStorage::Identification.new self
+ end
end
diff --git a/activestorage/app/models/active_storage/filename.rb b/activestorage/app/models/active_storage/filename.rb
index b9413dec95..bebb5e61b3 100644
--- a/activestorage/app/models/active_storage/filename.rb
+++ b/activestorage/app/models/active_storage/filename.rb
@@ -3,8 +3,18 @@
# Encapsulates a string representing a filename to provide convenient access to parts of it and sanitization.
# A Filename instance is returned by ActiveStorage::Blob#filename, and is comparable so it can be used for sorting.
class ActiveStorage::Filename
+ require_dependency "active_storage/filename/parameters"
+
include Comparable
+ class << self
+ # Returns a Filename instance based on the given filename. If the filename is a Filename, it is
+ # returned unmodified. If it is a String, it is passed to ActiveStorage::Filename.new.
+ def wrap(filename)
+ filename.kind_of?(self) ? filename : new(filename)
+ end
+ end
+
def initialize(filename)
@filename = filename
end
diff --git a/activestorage/app/models/active_storage/identification.rb b/activestorage/app/models/active_storage/identification.rb
index 4f295257ae..8d334ae1ea 100644
--- a/activestorage/app/models/active_storage/identification.rb
+++ b/activestorage/app/models/active_storage/identification.rb
@@ -1,25 +1,22 @@
# frozen_string_literal: true
-class ActiveStorage::Identification
+require "net/http"
+
+class ActiveStorage::Identification #:nodoc:
attr_reader :blob
def initialize(blob)
@blob = blob
end
- def apply
- blob.update!(content_type: content_type, identified: true) unless blob.identified?
+ def content_type
+ Marcel::MimeType.for(identifiable_chunk, name: filename, declared_type: declared_content_type)
end
private
- def content_type
- Marcel::MimeType.for(identifiable_chunk, name: filename, declared_type: declared_content_type)
- end
-
-
def identifiable_chunk
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client|
- client.get(uri, "Range" => "0-4096").body
+ client.get(uri, "Range" => "bytes=0-4095").body
end
end
diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb
index be5053edae..45efd26214 100644
--- a/activestorage/app/models/active_storage/preview.rb
+++ b/activestorage/app/models/active_storage/preview.rb
@@ -24,7 +24,7 @@
# The built-in previewers rely on third-party system libraries:
#
# * {ffmpeg}[https://www.ffmpeg.org]
-# * {mupdf}[https://mupdf.com]
+# * {mupdf}[https://mupdf.com] (version 1.8 or newer)
#
# These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you
# install and use third-party software, make sure you understand the licensing implications of doing so.
diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb
index e08a2271ec..a95a4bfd7c 100644
--- a/activestorage/app/models/active_storage/variant.rb
+++ b/activestorage/app/models/active_storage/variant.rb
@@ -115,7 +115,7 @@ class ActiveStorage::Variant
def download_image
require "mini_magick"
- MiniMagick::Image.create { |file| download_blob_to(file) }
+ MiniMagick::Image.create(blob.filename.extension_with_delimiter) { |file| download_blob_to(file) }
end
def transform(image)
diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb
index da4af62666..12e7f9f0b5 100644
--- a/activestorage/app/models/active_storage/variation.rb
+++ b/activestorage/app/models/active_storage/variation.rb
@@ -8,6 +8,14 @@
#
# ActiveStorage::Variation.new(resize: "100x100", monochrome: true, trim: true, rotate: "-90")
#
+# You can also combine multiple transformations in one step, e.g. for center-weighted cropping:
+#
+# ActiveStorage::Variation.new(combine_options: {
+# resize: "100x100^",
+# gravity: "center",
+# crop: "100x100+0+0",
+# })
+#
# A list of all possible transformations is available at https://www.imagemagick.org/script/mogrify.php.
class ActiveStorage::Variation
attr_reader :transformations
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index 8ba32490b1..1e223f9f17 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -18,7 +18,15 @@ module ActiveStorage
config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ]
config.active_storage.paths = ActiveSupport::OrderedOptions.new
- config.active_storage.variable_content_types = %w( image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop )
+ config.active_storage.variable_content_types = %w(
+ image/png
+ image/gif
+ image/jpg
+ image/jpeg
+ image/vnd.adobe.photoshop
+ image/vnd.microsoft.icon
+ )
+
config.active_storage.content_types_to_serve_as_binary = %w(
text/html
text/javascript
@@ -60,7 +68,7 @@ module ActiveStorage
end
initializer "active_storage.services" do
- config.to_prepare do
+ ActiveSupport.on_load(:active_storage_blob) do
if config_choice = Rails.configuration.active_storage.service
configs = Rails.configuration.active_storage.service_configurations ||= begin
config_file = Pathname.new(Rails.root.join("config/storage.yml"))
diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb
index 6f6f4105fe..d163605754 100644
--- a/activestorage/lib/active_storage/service/gcs_service.rb
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -80,10 +80,9 @@ module ActiveStorage
end
end
- def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:)
+ def url_for_direct_upload(key, expires_in:, checksum:, **)
instrument :url, key: key do |payload|
- generated_url = bucket.signed_url key, method: "PUT", expires: expires_in,
- content_type: content_type, content_md5: checksum
+ generated_url = bucket.signed_url key, method: "PUT", expires: expires_in, content_md5: checksum
payload[:url] = generated_url
@@ -91,8 +90,8 @@ module ActiveStorage
end
end
- def headers_for_direct_upload(key, content_type:, checksum:, **)
- { "Content-Type" => content_type, "Content-MD5" => checksum }
+ def headers_for_direct_upload(key, checksum:, **)
+ { "Content-Type" => "", "Content-MD5" => checksum }
end
private
diff --git a/activestorage/test/controllers/direct_uploads_controller_test.rb b/activestorage/test/controllers/direct_uploads_controller_test.rb
index 888767086c..5d57e37688 100644
--- a/activestorage/test/controllers/direct_uploads_controller_test.rb
+++ b/activestorage/test/controllers/direct_uploads_controller_test.rb
@@ -27,7 +27,7 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr
assert_equal checksum, details["checksum"]
assert_equal "text/plain", details["content_type"]
assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], details["direct_upload"]["url"]
- assert_match(/s3\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"])
+ assert_match(/s3(-[-a-z0-9]+)?\.(\S+)?amazonaws\.com/, details["direct_upload"]["url"])
assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
end
end
@@ -62,7 +62,7 @@ if SERVICE_CONFIGURATIONS[:gcs]
assert_equal checksum, details["checksum"]
assert_equal "text/plain", details["content_type"]
assert_match %r{storage\.googleapis\.com/#{@config[:bucket]}}, details["direct_upload"]["url"]
- assert_equal({ "Content-Type" => "text/plain", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
+ assert_equal({ "Content-Type" => "", "Content-MD5" => checksum }, details["direct_upload"]["headers"])
end
end
end
diff --git a/activestorage/test/fixtures/files/favicon.ico b/activestorage/test/fixtures/files/favicon.ico
new file mode 100644
index 0000000000..87192a8a07
--- /dev/null
+++ b/activestorage/test/fixtures/files/favicon.ico
Binary files differ
diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb
index 5cd2a94326..779e47ffb6 100644
--- a/activestorage/test/models/blob_test.rb
+++ b/activestorage/test/models/blob_test.rb
@@ -7,6 +7,23 @@ require "active_support/testing/method_call_assertions"
class ActiveStorage::BlobTest < ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
+ test ".unattached scope returns not attached blobs" do
+ class UserWithHasOneAttachedDependentFalse < User
+ has_one_attached :avatar, dependent: false
+ end
+
+ ActiveStorage::Blob.delete_all
+ blob_1 = create_blob filename: "funky.jpg"
+ blob_2 = create_blob filename: "town.jpg"
+
+ user = UserWithHasOneAttachedDependentFalse.create!
+ user.avatar.attach blob_1
+
+ assert_equal [blob_2], ActiveStorage::Blob.unattached
+ user.destroy
+ assert_equal [blob_1, blob_2].map(&:id).sort, ActiveStorage::Blob.unattached.pluck(:id).sort
+ end
+
test "create after upload sets byte size and checksum" do
data = "Hello world!"
blob = create_blob data: data
@@ -82,6 +99,8 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase
freeze_time do
assert_equal expected_url_for(blob), blob.service_url
assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: new_filename)
+ assert_equal expected_url_for(blob, filename: new_filename), blob.service_url(filename: "new.txt")
+ assert_equal expected_url_for(blob, filename: blob.filename), blob.service_url(filename: nil)
end
end
diff --git a/activestorage/test/models/variant_test.rb b/activestorage/test/models/variant_test.rb
index 0cf8a583bd..0f3ada25c0 100644
--- a/activestorage/test/models/variant_test.rb
+++ b/activestorage/test/models/variant_test.rb
@@ -50,6 +50,17 @@ class ActiveStorage::VariantTest < ActiveSupport::TestCase
assert_equal 20, image.height
end
+ test "resized variation of ICO blob" do
+ blob = create_file_blob(filename: "favicon.ico", content_type: "image/vnd.microsoft.icon")
+ variant = blob.variant(resize: "20x20").processed
+ assert_match(/icon\.png/, variant.service_url)
+
+ image = read_image(variant)
+ assert_equal "PNG", image.type
+ assert_equal 20, image.width
+ assert_equal 20, image.height
+ end
+
test "optimized variation of GIF blob" do
blob = create_file_blob(filename: "image.gif", content_type: "image/gif")
diff --git a/activestorage/test/service/gcs_service_test.rb b/activestorage/test/service/gcs_service_test.rb
index 7efcd60fb7..fc2d9d0fa7 100644
--- a/activestorage/test/service/gcs_service_test.rb
+++ b/activestorage/test/service/gcs_service_test.rb
@@ -19,7 +19,7 @@ if SERVICE_CONFIGURATIONS[:gcs]
uri = URI.parse url
request = Net::HTTP::Put.new uri.request_uri
request.body = data
- request.add_field "Content-Type", "text/plain"
+ request.add_field "Content-Type", ""
request.add_field "Content-MD5", checksum
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request request
diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb
index c3818422aa..d6996209d2 100644
--- a/activestorage/test/service/s3_service_test.rb
+++ b/activestorage/test/service/s3_service_test.rb
@@ -35,7 +35,7 @@ if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].pr
url = @service.url(FIXTURE_KEY, expires_in: 5.minutes,
disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
- assert_match(/s3\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url)
+ assert_match(/s3(-[-a-z0-9]+)?\.(\S+)?amazonaws.com.*response-content-disposition=inline.*avatar\.png.*response-content-type=image%2Fpng/, url)
assert_match SERVICE_CONFIGURATIONS[:s3][:bucket], url
end
diff --git a/activesupport/.gitignore b/activesupport/.gitignore
new file mode 100644
index 0000000000..8cde8514fd
--- /dev/null
+++ b/activesupport/.gitignore
@@ -0,0 +1 @@
+/test/fixtures/isolation_test/
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index c9cf63f7b5..be1862279c 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,56 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Add `private: true` option to ActiveSupport's `delegate`.
+
+ In order to delegate methods as private methods:
+
+ class User < ActiveRecord::Base
+ has_one :profile
+ delegate :date_of_birth, to: :profile, private: true
+
+ def age
+ Date.today.year - date_of_birth.year
+ end
+ end
+
+ # User.new.age # => 29
+ # User.new.date_of_birth
+ # => NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
+
+ More information in #31944.
+
+ *Tomas Valent*
+
+* `String#truncate_bytes` to truncate a string to a maximum bytesize without
+ breaking multibyte characters or grapheme clusters like 👩‍👩‍👦‍👦.
+
+ *Jeremy Daer*
+
+* `String#strip_heredoc` preserves frozenness.
+
+ "foo".freeze.strip_heredoc.frozen? # => true
+
+ Fixes that frozen string literals would inadvertently become unfrozen:
+
+ # frozen_string_literal: true
+
+ foo = <<-MSG.strip_heredoc
+ la la la
+ MSG
+
+ foo.frozen? # => false !??
+
+ *Jeremy Daer*
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
+
+* Adds parallel testing to Rails
+
+ Parallelize your test suite with forked processes or threads.
+
+ *Eileen M. Uchitelle*, *Aaron Patterson*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md) for previous changes.
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index db801d2da2..aa695c98b2 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework."
s.description = "A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
@@ -27,7 +27,7 @@ Gem::Specification.new do |s|
"changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activesupport/CHANGELOG.md"
}
- s.add_dependency "i18n", "~> 0.7"
+ s.add_dependency "i18n", ">= 0.7", "< 2"
s.add_dependency "tzinfo", "~> 1.1"
s.add_dependency "minitest", "~> 5.1"
s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables
deleted file mode 100755
index 18199b2171..0000000000
--- a/activesupport/bin/generate_tables
+++ /dev/null
@@ -1,141 +0,0 @@
-#!/usr/bin/env ruby
-# frozen_string_literal: true
-
-begin
- $:.unshift(File.expand_path("../lib", __dir__))
- require "active_support"
-rescue IOError
-end
-
-require "open-uri"
-require "tmpdir"
-require "fileutils"
-
-module ActiveSupport
- module Multibyte
- module Unicode
- class UnicodeDatabase
- def load; end
- end
-
- class DatabaseGenerator
- BASE_URI = "http://www.unicode.org/Public/#{UNICODE_VERSION}/ucd/"
- SOURCES = {
- codepoints: BASE_URI + "UnicodeData.txt",
- composition_exclusion: BASE_URI + "CompositionExclusions.txt",
- grapheme_break_property: BASE_URI + "auxiliary/GraphemeBreakProperty.txt",
- cp1252: "http://unicode.org/Public/MAPPINGS/VENDORS/MICSFT/WINDOWS/CP1252.TXT"
- }
-
- def initialize
- @ucd = Unicode::UnicodeDatabase.new
- end
-
- def parse_codepoints(line)
- codepoint = Codepoint.new
- raise "Could not parse input." unless line =~ /^
- ([0-9A-F]+); # code
- ([^;]+); # name
- ([A-Z]+); # general category
- ([0-9]+); # canonical combining class
- ([A-Z]+); # bidi class
- (<([A-Z]*)>)? # decomposition type
- ((\ ?[0-9A-F]+)*); # decomposition mapping
- ([0-9]*); # decimal digit
- ([0-9]*); # digit
- ([^;]*); # numeric
- ([YN]*); # bidi mirrored
- ([^;]*); # unicode 1.0 name
- ([^;]*); # iso comment
- ([0-9A-F]*); # simple uppercase mapping
- ([0-9A-F]*); # simple lowercase mapping
- ([0-9A-F]*)$/ix # simple titlecase mapping
- codepoint.code = $1.hex
- codepoint.combining_class = Integer($4)
- codepoint.decomp_type = $7
- codepoint.decomp_mapping = ($8 == "") ? nil : $8.split.collect(&:hex)
- codepoint.uppercase_mapping = ($16 == "") ? 0 : $16.hex
- codepoint.lowercase_mapping = ($17 == "") ? 0 : $17.hex
- @ucd.codepoints[codepoint.code] = codepoint
- end
-
- def parse_grapheme_break_property(line)
- if line =~ /^([0-9A-F.]+)\s*;\s*([\w]+)\s*#/
- type = $2.downcase.intern
- @ucd.boundary[type] ||= []
- if $1.include? ".."
- parts = $1.split ".."
- @ucd.boundary[type] << (parts[0].hex..parts[1].hex)
- else
- @ucd.boundary[type] << $1.hex
- end
- end
- end
-
- def parse_composition_exclusion(line)
- if line =~ /^([0-9A-F]+)/i
- @ucd.composition_exclusion << $1.hex
- end
- end
-
- def parse_cp1252(line)
- if line =~ /^([0-9A-Fx]+)\s([0-9A-Fx]+)/i
- @ucd.cp1252[$1.hex] = $2.hex
- end
- end
-
- def create_composition_map
- @ucd.codepoints.each do |_, cp|
- if !cp.nil? && cp.combining_class == 0 && cp.decomp_type.nil? && !cp.decomp_mapping.nil? && cp.decomp_mapping.length == 2 && @ucd.codepoints[cp.decomp_mapping[0]].combining_class == 0 && !@ucd.composition_exclusion.include?(cp.code)
- @ucd.composition_map[cp.decomp_mapping[0]] ||= {}
- @ucd.composition_map[cp.decomp_mapping[0]][cp.decomp_mapping[1]] = cp.code
- end
- end
- end
-
- def normalize_boundary_map
- @ucd.boundary.each do |k, v|
- if [:lf, :cr].include? k
- @ucd.boundary[k] = v[0]
- end
- end
- end
-
- def parse
- SOURCES.each do |type, url|
- filename = File.join(Dir.tmpdir, UNICODE_VERSION, "#{url.split('/').last}")
- unless File.exist?(filename)
- $stderr.puts "Downloading #{url.split('/').last}"
- FileUtils.mkdir_p(File.dirname(filename))
- File.open(filename, "wb") do |target|
- open(url) do |source|
- source.each_line { |line| target.write line }
- end
- end
- end
- File.open(filename) do |file|
- file.each_line { |line| send "parse_#{type}".intern, line }
- end
- end
- create_composition_map
- normalize_boundary_map
- end
-
- def dump_to(filename)
- File.open(filename, "wb") do |f|
- f.write Marshal.dump([@ucd.codepoints, @ucd.composition_exclusion, @ucd.composition_map, @ucd.boundary, @ucd.cp1252])
- end
- end
- end
- end
- end
-end
-
-if __FILE__ == $0
- filename = ActiveSupport::Multibyte::Unicode::UnicodeDatabase.filename
- generator = ActiveSupport::Multibyte::Unicode::DatabaseGenerator.new
- generator.parse
- print "Writing to: #{filename}"
- generator.dump_to filename
- puts " (#{File.size(filename)} bytes)"
-end
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 2d038dba77..1ea2d0bbf2 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -160,6 +160,23 @@ module ActiveSupport
attr_reader :silence, :options
alias :silence? :silence
+ class << self
+ private
+ def retrieve_pool_options(options)
+ {}.tap do |pool_options|
+ pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
+ pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
+ end
+ end
+
+ def ensure_connection_pool_added!
+ require "connection_pool"
+ rescue LoadError => e
+ $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
+ raise e
+ end
+ end
+
# Creates a new cache. The options will be passed to any write method calls
# except for <tt>:namespace</tt> which can be used to set the global
# namespace for the cache.
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index cae0d44e7d..2840781dde 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -63,21 +63,12 @@ module ActiveSupport
addresses = addresses.flatten
options = addresses.extract_options!
addresses = ["localhost:11211"] if addresses.empty?
-
- pool_options = {}
- pool_options[:size] = options[:pool_size] if options[:pool_size]
- pool_options[:timeout] = options[:pool_timeout] if options[:pool_timeout]
+ pool_options = retrieve_pool_options(options)
if pool_options.empty?
Dalli::Client.new(addresses, options)
else
- begin
- require "connection_pool"
- rescue LoadError => e
- $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
- raise e
- end
-
+ ensure_connection_pool_added!
ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
end
end
diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb
index af14c28408..64bc77cf22 100644
--- a/activesupport/lib/active_support/cache/redis_cache_store.rb
+++ b/activesupport/lib/active_support/cache/redis_cache_store.rb
@@ -17,10 +17,18 @@ end
require "digest/sha2"
require "active_support/core_ext/marshal"
-require "active_support/core_ext/hash/transform_values"
module ActiveSupport
module Cache
+ module ConnectionPoolLike
+ def with
+ yield self
+ end
+ end
+
+ ::Redis.include(ConnectionPoolLike)
+ ::Redis::Distributed.include(ConnectionPoolLike)
+
# Redis cache store.
#
# Deployment note: Take care to use a *dedicated Redis cache* rather
@@ -150,7 +158,7 @@ module ActiveSupport
#
# Compression is enabled by default with a 1kB threshold, so cached
# values larger than 1kB are automatically compressed. Disable by
- # passing <tt>cache: false</tt> or change the threshold by passing
+ # passing <tt>compress: false</tt> or change the threshold by passing
# <tt>compress_threshold: 4.kilobytes</tt>.
#
# No expiry is set on cache entries by default. Redis is expected to
@@ -173,7 +181,16 @@ module ActiveSupport
end
def redis
- @redis ||= self.class.build_redis(**redis_options)
+ @redis ||= begin
+ pool_options = self.class.send(:retrieve_pool_options, redis_options)
+
+ if pool_options.any?
+ self.class.send(:ensure_connection_pool_added!)
+ ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
+ else
+ self.class.build_redis(**redis_options)
+ end
+ end
end
def inspect
@@ -216,7 +233,7 @@ module ActiveSupport
instrument :delete_matched, matcher do
case matcher
when String
- redis.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)]
+ redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] }
else
raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
end
@@ -234,7 +251,7 @@ module ActiveSupport
def increment(name, amount = 1, options = nil)
instrument :increment, name, amount: amount do
failsafe :increment do
- redis.incrby normalize_key(name, options), amount
+ redis.with { |c| c.incrby normalize_key(name, options), amount }
end
end
end
@@ -250,7 +267,7 @@ module ActiveSupport
def decrement(name, amount = 1, options = nil)
instrument :decrement, name, amount: amount do
failsafe :decrement do
- redis.decrby normalize_key(name, options), amount
+ redis.with { |c| c.decrby normalize_key(name, options), amount }
end
end
end
@@ -272,7 +289,7 @@ module ActiveSupport
if namespace = merged_options(options)[namespace]
delete_matched "*", namespace: namespace
else
- redis.flushdb
+ redis.with { |c| c.flushdb }
end
end
end
@@ -303,7 +320,7 @@ module ActiveSupport
# Read an entry from the cache.
def read_entry(key, options = nil)
failsafe :read_entry do
- deserialize_entry redis.get(key)
+ deserialize_entry redis.with { |c| c.get(key) }
end
end
@@ -322,7 +339,7 @@ module ActiveSupport
keys = names.map { |name| normalize_key(name, options) }
values = failsafe(:read_multi_mget, returning: {}) do
- redis.mget(*keys)
+ redis.with { |c| c.mget(*keys) }
end
names.zip(values).each_with_object({}) do |(name, value), results|
@@ -354,9 +371,9 @@ module ActiveSupport
modifiers[:nx] = unless_exist
modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
- redis.set key, serialized_entry, modifiers
+ redis.with { |c| c.set key, serialized_entry, modifiers }
else
- redis.set key, serialized_entry
+ redis.with { |c| c.set key, serialized_entry }
end
end
end
@@ -364,7 +381,7 @@ module ActiveSupport
# Delete an entry from the cache.
def delete_entry(key, options)
failsafe :delete_entry, returning: false do
- redis.del key
+ redis.with { |c| c.del key }
end
end
@@ -373,7 +390,7 @@ module ActiveSupport
if entries.any?
if mset_capable? && expires_in.nil?
failsafe :write_multi_entries do
- redis.mapped_mset(serialize_entries(entries, raw: options[:raw]))
+ redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) }
end
else
super
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index aaa9638fa8..e17308f83e 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -54,6 +54,10 @@ module ActiveSupport
@data[key]
end
+ def read_multi_entries(keys, options)
+ Hash[keys.map { |name| [name, read_entry(name, options)] }.keep_if { |_name, value| value }]
+ end
+
def write_entry(key, value, options)
@data[key] = value
true
@@ -116,6 +120,19 @@ module ActiveSupport
end
end
+ def read_multi_entries(keys, options)
+ return super unless local_cache
+
+ local_entries = local_cache.read_multi_entries(keys, options)
+ missed_keys = keys - local_entries.keys
+
+ if missed_keys.any?
+ local_entries.merge!(super(missed_keys, options))
+ else
+ local_entries
+ end
+ end
+
def write_entry(key, entry, options)
if options[:unless_exist]
local_cache.delete_entry(key, options) if local_cache
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index 7928efb871..fa33ff945f 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -98,7 +98,7 @@ class Class
singleton_class.silence_redefinition_of_method("#{name}?")
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
- ivar = "@#{name}"
+ ivar = "@#{name}".to_sym
singleton_class.silence_redefinition_of_method("#{name}=")
define_singleton_method("#{name}=") do |val|
diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb
index 17733d955c..f01d01e6aa 100644
--- a/activesupport/lib/active_support/core_ext/enumerable.rb
+++ b/activesupport/lib/active_support/core_ext/enumerable.rb
@@ -3,50 +3,37 @@
module Enumerable
# Enumerable#sum was added in Ruby 2.4, but it only works with Numeric elements
# when we omit an identity.
+
+ # We can't use Refinements here because Refinements with Module which will be prepended
+ # doesn't work well https://bugs.ruby-lang.org/issues/13446
+ alias :_original_sum_with_required_identity :sum
+ private :_original_sum_with_required_identity
+
+ # Calculates a sum from the elements.
#
- # We tried shimming it to attempt the fast native method, rescue TypeError,
- # and fall back to the compatible implementation, but that's much slower than
- # just calling the compat method in the first place.
- if Enumerable.instance_methods(false).include?(:sum) && !((?a..?b).sum rescue false)
- # We can't use Refinements here because Refinements with Module which will be prepended
- # doesn't work well https://bugs.ruby-lang.org/issues/13446
- alias :_original_sum_with_required_identity :sum
- private :_original_sum_with_required_identity
- # Calculates a sum from the elements.
- #
- # payments.sum { |p| p.price * p.tax_rate }
- # payments.sum(&:price)
- #
- # The latter is a shortcut for:
- #
- # payments.inject(0) { |sum, p| sum + p.price }
- #
- # It can also calculate the sum without the use of a block.
- #
- # [5, 15, 10].sum # => 30
- # ['foo', 'bar'].sum # => "foobar"
- # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5]
- #
- # The default sum of an empty list is zero. You can override this default:
- #
- # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
- def sum(identity = nil, &block)
- if identity
- _original_sum_with_required_identity(identity, &block)
- elsif block_given?
- map(&block).sum(identity)
- else
- inject(:+) || 0
- end
- end
- else
- def sum(identity = nil, &block)
- if block_given?
- map(&block).sum(identity)
- else
- sum = identity ? inject(identity, :+) : inject(:+)
- sum || identity || 0
- end
+ # payments.sum { |p| p.price * p.tax_rate }
+ # payments.sum(&:price)
+ #
+ # The latter is a shortcut for:
+ #
+ # payments.inject(0) { |sum, p| sum + p.price }
+ #
+ # It can also calculate the sum without the use of a block.
+ #
+ # [5, 15, 10].sum # => 30
+ # ['foo', 'bar'].sum # => "foobar"
+ # [[1, 2], [3, 1, 5]].sum # => [1, 2, 3, 1, 5]
+ #
+ # The default sum of an empty list is zero. You can override this default:
+ #
+ # [].sum(Payment.new(0)) { |i| i.amount } # => Payment.new(0)
+ def sum(identity = nil, &block)
+ if identity
+ _original_sum_with_required_identity(identity, &block)
+ elsif block_given?
+ map(&block).sum(identity)
+ else
+ inject(:+) || 0
end
end
@@ -133,27 +120,21 @@ class Range #:nodoc:
end
end
-# Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
-#
-# We tried shimming it to attempt the fast native method, rescue TypeError,
-# and fall back to the compatible implementation, but that's much slower than
-# just calling the compat method in the first place.
-if Array.instance_methods(false).include?(:sum) && !(%w[a].sum rescue false)
- # Using Refinements here in order not to expose our internal method
- using Module.new {
- refine Array do
- alias :orig_sum :sum
- end
- }
+# Using Refinements here in order not to expose our internal method
+using Module.new {
+ refine Array do
+ alias :orig_sum :sum
+ end
+}
- class Array
- def sum(init = nil, &block) #:nodoc:
- if init.is_a?(Numeric) || first.is_a?(Numeric)
- init ||= 0
- orig_sum(init, &block)
- else
- super
- end
+class Array #:nodoc:
+ # Array#sum was added in Ruby 2.4 but it only works with Numeric elements.
+ def sum(init = nil, &block)
+ if init.is_a?(Numeric) || first.is_a?(Numeric)
+ init ||= 0
+ orig_sum(init, &block)
+ else
+ super
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index e19aeaa983..325581a60a 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -8,4 +8,3 @@ require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/hash/keys"
require "active_support/core_ext/hash/reverse_merge"
require "active_support/core_ext/hash/slice"
-require "active_support/core_ext/hash/transform_values"
diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
index 4b19c9fc1f..fc15130c9e 100644
--- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -1,32 +1,5 @@
# frozen_string_literal: true
-class Hash
- # Returns a new hash with the results of running +block+ once for every value.
- # The keys are unchanged.
- #
- # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 }
- #
- # If you do not provide a +block+, it will return an Enumerator
- # for chaining with other methods:
- #
- # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 }
- def transform_values
- return enum_for(:transform_values) { size } unless block_given?
- return {} if empty?
- result = self.class.new
- each do |key, value|
- result[key] = yield(value)
- end
- result
- end unless method_defined? :transform_values
+require "active_support/deprecation"
- # Destructively converts all values using the +block+ operations.
- # Same as +transform_values+ but modifies +self+.
- def transform_values!
- return enum_for(:transform_values!) { size } unless block_given?
- each do |key, value|
- self[key] = yield(value)
- end
- end unless method_defined? :transform_values!
- # TODO: Remove this file when supporting only Ruby 2.4+.
-end
+ActiveSupport::Deprecation.warn "Ruby 2.4+ (required by Rails 6) provides Hash#transform_values natively, so requiring active_support/core_ext/hash/transform_values is no longer necessary. Requiring it will raise LoadError in Rails 6.1."
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 4310df3024..426e34128f 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -114,6 +114,24 @@ class Module
# invoice.customer_name # => 'John Doe'
# invoice.customer_address # => 'Vimmersvej 13'
#
+ # If you want the delegated method to be a private method,
+ # use the <tt>:private</tt> option.
+ #
+ # class User < ActiveRecord::Base
+ # has_one :profile
+ # delegate :first_name, to: :profile
+ # delegate :date_of_birth, :religion, to: :profile, private: true
+ #
+ # def age
+ # Date.today.year - date_of_birth.year
+ # end
+ # end
+ #
+ # User.new.age # 2
+ # User.new.first_name # Tomas
+ # User.new.date_of_birth # NoMethodError: private method `date_of_birth' called for #<User:0x00000008221340>
+ # User.new.religion # NoMethodError: private method `religion' called for #<User:0x00000008221340>
+ #
# If the target is +nil+ and does not respond to the delegated method a
# +Module::DelegationError+ is raised. If you wish to instead return +nil+,
# use the <tt>:allow_nil</tt> option.
@@ -151,7 +169,7 @@ class Module
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
#
# The target method must be public, otherwise it will raise +NoMethodError+.
- def delegate(*methods, to: nil, prefix: nil, allow_nil: nil)
+ def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil)
unless to
raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)."
end
@@ -173,7 +191,7 @@ class Module
to = to.to_s
to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to)
- methods.map do |method|
+ method_names = methods.map do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block"
@@ -213,6 +231,9 @@ class Module
module_eval(method_def, file, line)
end
+
+ private(*method_names) if private
+ method_names
end
# When building decorators, a common pattern may emerge:
diff --git a/activesupport/lib/active_support/core_ext/module/redefine_method.rb b/activesupport/lib/active_support/core_ext/module/redefine_method.rb
index a0a6622ca4..5bd8e6e973 100644
--- a/activesupport/lib/active_support/core_ext/module/redefine_method.rb
+++ b/activesupport/lib/active_support/core_ext/module/redefine_method.rb
@@ -1,23 +1,14 @@
# frozen_string_literal: true
class Module
- if RUBY_VERSION >= "2.3"
- # Marks the named method as intended to be redefined, if it exists.
- # Suppresses the Ruby method redefinition warning. Prefer
- # #redefine_method where possible.
- def silence_redefinition_of_method(method)
- if method_defined?(method) || private_method_defined?(method)
- # This suppresses the "method redefined" warning; the self-alias
- # looks odd, but means we don't need to generate a unique name
- alias_method method, method
- end
- end
- else
- def silence_redefinition_of_method(method)
- if method_defined?(method) || private_method_defined?(method)
- alias_method :__rails_redefine, method
- remove_method :__rails_redefine
- end
+ # Marks the named method as intended to be redefined, if it exists.
+ # Suppresses the Ruby method redefinition warning. Prefer
+ # #redefine_method where possible.
+ def silence_redefinition_of_method(method)
+ if method_defined?(method) || private_method_defined?(method)
+ # This suppresses the "method redefined" warning; the self-alias
+ # looks odd, but means we don't need to generate a unique name
+ alias_method method, method
end
end
diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
index f6c2713986..7fcd0d0311 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -129,12 +129,6 @@ module ActiveSupport::NumericWithFormat
end
end
-# Ruby 2.4+ unifies Fixnum & Bignum into Integer.
-if 0.class == Integer
- Integer.prepend ActiveSupport::NumericWithFormat
-else
- Fixnum.prepend ActiveSupport::NumericWithFormat
- Bignum.prepend ActiveSupport::NumericWithFormat
-end
+Integer.prepend ActiveSupport::NumericWithFormat
Float.prepend ActiveSupport::NumericWithFormat
BigDecimal.prepend ActiveSupport::NumericWithFormat
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index 9bb99087bc..c78ee6bbfc 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -75,8 +75,11 @@ end
class Symbol
begin
- :symbol.dup # Ruby 2.4.x.
- "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0.
+ :symbol.dup
+
+ # Some symbols couldn't be duped in Ruby 2.4.0 only, due to a bug.
+ # This feature check catches any regression.
+ "symbol_from_string".to_sym.dup
rescue TypeError
# Symbols are not duplicable:
diff --git a/activesupport/lib/active_support/core_ext/regexp.rb b/activesupport/lib/active_support/core_ext/regexp.rb
index efbd708aee..d92943c7ae 100644
--- a/activesupport/lib/active_support/core_ext/regexp.rb
+++ b/activesupport/lib/active_support/core_ext/regexp.rb
@@ -4,8 +4,4 @@ class Regexp #:nodoc:
def multiline?
options & MULTILINE == MULTILINE
end
-
- def match?(string, pos = 0)
- !!match(string, pos)
- end unless //.respond_to?(:match?)
end
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index 66e721eea3..df0e79afa8 100644
--- a/activesupport/lib/active_support/core_ext/string/filters.rb
+++ b/activesupport/lib/active_support/core_ext/string/filters.rb
@@ -78,6 +78,47 @@ class String
"#{self[0, stop]}#{omission}"
end
+ # Truncates +text+ to at most <tt>bytesize</tt> bytes in length without
+ # breaking string encoding by splitting multibyte characters or breaking
+ # grapheme clusters ("perceptual characters") by truncating at combining
+ # characters.
+ #
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".size
+ # => 20
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".bytesize
+ # => 80
+ # >> "🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪🔪".truncate_bytes(20)
+ # => "🔪🔪🔪🔪…"
+ #
+ # The truncated text ends with the <tt>:omission</tt> string, defaulting
+ # to "…", for a total length not exceeding <tt>bytesize</tt>.
+ def truncate_bytes(truncate_at, omission: "…")
+ omission ||= ""
+
+ case
+ when bytesize <= truncate_at
+ dup
+ when omission.bytesize > truncate_at
+ raise ArgumentError, "Omission #{omission.inspect} is #{omission.bytesize}, larger than the truncation length of #{truncate_at} bytes"
+ when omission.bytesize == truncate_at
+ omission.dup
+ else
+ self.class.new.tap do |cut|
+ cut_at = truncate_at - omission.bytesize
+
+ scan(/\X/) do |grapheme|
+ if cut.bytesize + grapheme.bytesize <= cut_at
+ cut << grapheme
+ else
+ break
+ end
+ end
+
+ cut << omission
+ end
+ end
+ end
+
# Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
#
# 'Once upon a time in a world far far away'.truncate_words(4)
diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb
index 07c0d16398..6cceb46507 100644
--- a/activesupport/lib/active_support/core_ext/string/multibyte.rb
+++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb
@@ -11,12 +11,13 @@ class String
# encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
# class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
#
- # >> "lj".upcase
- # => "lj"
# >> "lj".mb_chars.upcase.to_s
# => "LJ"
#
- # NOTE: An above example is useful for pre Ruby 2.4. Ruby 2.4 supports Unicode case mappings.
+ # NOTE: Ruby 2.4 and later support native Unicode case mappings:
+ #
+ # >> "lj".upcase
+ # => "LJ"
#
# == Method chaining
#
diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb
index cc26274e4a..6f9834bb16 100644
--- a/activesupport/lib/active_support/core_ext/string/strip.rb
+++ b/activesupport/lib/active_support/core_ext/string/strip.rb
@@ -20,6 +20,8 @@ class String
# Technically, it looks for the least indented non-empty line
# in the whole string, and removes that amount of leading whitespace.
def strip_heredoc
- gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze)
+ gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, "".freeze).tap do |stripped|
+ stripped.freeze if frozen?
+ end
end
end
diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb
index 581db5f449..66d6f3225a 100644
--- a/activesupport/lib/active_support/deprecation/behaviors.rb
+++ b/activesupport/lib/active_support/deprecation/behaviors.rb
@@ -85,7 +85,7 @@ module ActiveSupport
# ActiveSupport::Deprecation.behavior = :stderr
# ActiveSupport::Deprecation.behavior = [:stderr, :log]
# ActiveSupport::Deprecation.behavior = MyCustomHandler
- # ActiveSupport::Deprecation.behavior = ->(message, callstack) {
+ # ActiveSupport::Deprecation.behavior = ->(message, callstack, deprecation_horizon, gem_name) {
# # custom stuff
# }
def behavior=(behavior)
diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb
index bb177ae5b7..84ae29c1ec 100644
--- a/activesupport/lib/active_support/duration/iso8601_serializer.rb
+++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/object/blank"
-require "active_support/core_ext/hash/transform_values"
module ActiveSupport
class Duration
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 2e2ed8a25d..e4afc8af93 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -177,20 +177,18 @@ module ActiveSupport
super(convert_key(key), *extras)
end
- if Hash.new.respond_to?(:dig)
- # Same as <tt>Hash#dig</tt> where the key passed as argument can be
- # either a string or a symbol:
- #
- # counters = ActiveSupport::HashWithIndifferentAccess.new
- # counters[:foo] = { bar: 1 }
- #
- # counters.dig('foo', 'bar') # => 1
- # counters.dig(:foo, :bar) # => 1
- # counters.dig(:zoo) # => nil
- def dig(*args)
- args[0] = convert_key(args[0]) if args.size > 0
- super(*args)
- end
+ # Same as <tt>Hash#dig</tt> where the key passed as argument can be
+ # either a string or a symbol:
+ #
+ # counters = ActiveSupport::HashWithIndifferentAccess.new
+ # counters[:foo] = { bar: 1 }
+ #
+ # counters.dig('foo', 'bar') # => 1
+ # counters.dig(:foo, :bar) # => 1
+ # counters.dig(:zoo) # => nil
+ def dig(*args)
+ args[0] = convert_key(args[0]) if args.size > 0
+ super(*args)
end
# Same as <tt>Hash#default</tt> where the key passed as argument can be
@@ -228,7 +226,7 @@ module ActiveSupport
# hash.fetch_values('a', 'c') # => KeyError: key not found: "c"
def fetch_values(*indices, &block)
indices.collect { |key| fetch(key, &block) }
- end if Hash.method_defined?(:fetch_values)
+ end
# Returns a shallow copy of the hash.
#
@@ -311,6 +309,14 @@ module ActiveSupport
dup.tap { |hash| hash.transform_keys!(*args, &block) }
end
+ def transform_keys!
+ return enum_for(:transform_keys!) { size } unless block_given?
+ keys.each do |key|
+ self[yield(key)] = delete(key)
+ end
+ self
+ end
+
def slice(*keys)
keys.map! { |key| convert_key(key) }
self.class.new(super)
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index f923061fae..4f0e1165ef 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -11,7 +11,7 @@ module ActiveSupport
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
# The Unicode version that is supported by the implementation
- UNICODE_VERSION = "9.0.0"
+ UNICODE_VERSION = RbConfig::CONFIG["UNICODE_VERSION"]
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
@@ -21,96 +21,13 @@ module ActiveSupport
attr_accessor :default_normalization_form
@default_normalization_form = :kc
- # Hangul character boundaries and properties
- HANGUL_SBASE = 0xAC00
- HANGUL_LBASE = 0x1100
- HANGUL_VBASE = 0x1161
- HANGUL_TBASE = 0x11A7
- HANGUL_LCOUNT = 19
- HANGUL_VCOUNT = 21
- HANGUL_TCOUNT = 28
- HANGUL_NCOUNT = HANGUL_VCOUNT * HANGUL_TCOUNT
- HANGUL_SCOUNT = 11172
- HANGUL_SLAST = HANGUL_SBASE + HANGUL_SCOUNT
-
- # Detect whether the codepoint is in a certain character class. Returns
- # +true+ when it's in the specified character class and +false+ otherwise.
- # Valid character classes are: <tt>:cr</tt>, <tt>:lf</tt>, <tt>:l</tt>,
- # <tt>:v</tt>, <tt>:lv</tt>, <tt>:lvt</tt> and <tt>:t</tt>.
- #
- # Primarily used by the grapheme cluster support.
- def in_char_class?(codepoint, classes)
- classes.detect { |c| database.boundary[c] === codepoint } ? true : false
- end
-
# Unpack the string at grapheme boundaries. Returns a list of character
# lists.
#
# Unicode.unpack_graphemes('क्षि') # => [[2325, 2381], [2359], [2367]]
# Unicode.unpack_graphemes('Café') # => [[67], [97], [102], [233]]
def unpack_graphemes(string)
- codepoints = string.codepoints.to_a
- unpacked = []
- pos = 0
- marker = 0
- eoc = codepoints.length
- while (pos < eoc)
- pos += 1
- previous = codepoints[pos - 1]
- current = codepoints[pos]
-
- # See http://unicode.org/reports/tr29/#Grapheme_Cluster_Boundary_Rules
- should_break =
- if pos == eoc
- true
- # GB3. CR X LF
- elsif previous == database.boundary[:cr] && current == database.boundary[:lf]
- false
- # GB4. (Control|CR|LF) ÷
- elsif previous && in_char_class?(previous, [:control, :cr, :lf])
- true
- # GB5. ÷ (Control|CR|LF)
- elsif in_char_class?(current, [:control, :cr, :lf])
- true
- # GB6. L X (L|V|LV|LVT)
- elsif database.boundary[:l] === previous && in_char_class?(current, [:l, :v, :lv, :lvt])
- false
- # GB7. (LV|V) X (V|T)
- elsif in_char_class?(previous, [:lv, :v]) && in_char_class?(current, [:v, :t])
- false
- # GB8. (LVT|T) X (T)
- elsif in_char_class?(previous, [:lvt, :t]) && database.boundary[:t] === current
- false
- # GB9. X (Extend | ZWJ)
- elsif in_char_class?(current, [:extend, :zwj])
- false
- # GB9a. X SpacingMark
- elsif database.boundary[:spacingmark] === current
- false
- # GB9b. Prepend X
- elsif database.boundary[:prepend] === previous
- false
- # GB10. (E_Base | EBG) Extend* X E_Modifier
- elsif (marker...pos).any? { |i| in_char_class?(codepoints[i], [:e_base, :e_base_gaz]) && codepoints[i + 1...pos].all? { |c| database.boundary[:extend] === c } } && database.boundary[:e_modifier] === current
- false
- # GB11. ZWJ X (Glue_After_Zwj | EBG)
- elsif database.boundary[:zwj] === previous && in_char_class?(current, [:glue_after_zwj, :e_base_gaz])
- false
- # GB12. ^ (RI RI)* RI X RI
- # GB13. [^RI] (RI RI)* RI X RI
- elsif codepoints[marker..pos].all? { |c| database.boundary[:regional_indicator] === c } && codepoints[marker..pos].count { |c| database.boundary[:regional_indicator] === c }.even?
- false
- # GB999. Any ÷ Any
- else
- true
- end
-
- if should_break
- unpacked << codepoints[marker..pos - 1]
- marker = pos
- end
- end
- unpacked
+ string.scan(/\X/).map(&:codepoints)
end
# Reverse operation of unpack_graphemes.
@@ -120,100 +37,18 @@ module ActiveSupport
unpacked.flatten.pack("U*")
end
- # Re-order codepoints so the string becomes canonical.
- def reorder_characters(codepoints)
- length = codepoints.length - 1
- pos = 0
- while pos < length do
- cp1, cp2 = database.codepoints[codepoints[pos]], database.codepoints[codepoints[pos + 1]]
- if (cp1.combining_class > cp2.combining_class) && (cp2.combining_class > 0)
- codepoints[pos..pos + 1] = cp2.code, cp1.code
- pos += (pos > 0 ? -1 : 1)
- else
- pos += 1
- end
- end
- codepoints
- end
-
# Decompose composed characters to the decomposed form.
def decompose(type, codepoints)
- codepoints.inject([]) do |decomposed, cp|
- # if it's a hangul syllable starter character
- if HANGUL_SBASE <= cp && cp < HANGUL_SLAST
- sindex = cp - HANGUL_SBASE
- ncp = [] # new codepoints
- ncp << HANGUL_LBASE + sindex / HANGUL_NCOUNT
- ncp << HANGUL_VBASE + (sindex % HANGUL_NCOUNT) / HANGUL_TCOUNT
- tindex = sindex % HANGUL_TCOUNT
- ncp << (HANGUL_TBASE + tindex) unless tindex == 0
- decomposed.concat ncp
- # if the codepoint is decomposable in with the current decomposition type
- elsif (ncp = database.codepoints[cp].decomp_mapping) && (!database.codepoints[cp].decomp_type || type == :compatibility)
- decomposed.concat decompose(type, ncp.dup)
- else
- decomposed << cp
- end
+ if type == :compatibility
+ codepoints.pack("U*").unicode_normalize(:nfkd).codepoints
+ else
+ codepoints.pack("U*").unicode_normalize(:nfd).codepoints
end
end
# Compose decomposed characters to the composed form.
def compose(codepoints)
- pos = 0
- eoa = codepoints.length - 1
- starter_pos = 0
- starter_char = codepoints[0]
- previous_combining_class = -1
- while pos < eoa
- pos += 1
- lindex = starter_char - HANGUL_LBASE
- # -- Hangul
- if 0 <= lindex && lindex < HANGUL_LCOUNT
- vindex = codepoints[starter_pos + 1] - HANGUL_VBASE rescue vindex = -1
- if 0 <= vindex && vindex < HANGUL_VCOUNT
- tindex = codepoints[starter_pos + 2] - HANGUL_TBASE rescue tindex = -1
- if 0 <= tindex && tindex < HANGUL_TCOUNT
- j = starter_pos + 2
- eoa -= 2
- else
- tindex = 0
- j = starter_pos + 1
- eoa -= 1
- end
- codepoints[starter_pos..j] = (lindex * HANGUL_VCOUNT + vindex) * HANGUL_TCOUNT + tindex + HANGUL_SBASE
- end
- starter_pos += 1
- starter_char = codepoints[starter_pos]
- # -- Other characters
- else
- current_char = codepoints[pos]
- current = database.codepoints[current_char]
- if current.combining_class > previous_combining_class
- if ref = database.composition_map[starter_char]
- composition = ref[current_char]
- else
- composition = nil
- end
- unless composition.nil?
- codepoints[starter_pos] = composition
- starter_char = composition
- codepoints.delete_at pos
- eoa -= 1
- pos -= 1
- previous_combining_class = -1
- else
- previous_combining_class = current.combining_class
- end
- else
- previous_combining_class = current.combining_class
- end
- if current.combining_class == 0
- starter_pos = pos
- starter_char = codepoints[pos]
- end
- end
- end
- codepoints
+ codepoints.pack("U*").unicode_normalize(:nfc).codepoints
end
# Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
@@ -266,129 +101,37 @@ module ActiveSupport
def normalize(string, form = nil)
form ||= @default_normalization_form
# See http://www.unicode.org/reports/tr15, Table 1
- codepoints = string.codepoints.to_a
case form
when :d
- reorder_characters(decompose(:canonical, codepoints))
+ string.unicode_normalize(:nfd)
when :c
- compose(reorder_characters(decompose(:canonical, codepoints)))
+ string.unicode_normalize(:nfc)
when :kd
- reorder_characters(decompose(:compatibility, codepoints))
+ string.unicode_normalize(:nfkd)
when :kc
- compose(reorder_characters(decompose(:compatibility, codepoints)))
+ string.unicode_normalize(:nfkc)
else
raise ArgumentError, "#{form} is not a valid normalization variant", caller
- end.pack("U*".freeze)
+ end
end
def downcase(string)
- apply_mapping string, :lowercase_mapping
+ string.downcase
end
def upcase(string)
- apply_mapping string, :uppercase_mapping
+ string.upcase
end
def swapcase(string)
- apply_mapping string, :swapcase_mapping
- end
-
- # Holds data about a codepoint in the Unicode database.
- class Codepoint
- attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
-
- # Initializing Codepoint object with default values
- def initialize
- @combining_class = 0
- @uppercase_mapping = 0
- @lowercase_mapping = 0
- end
-
- def swapcase_mapping
- uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
- end
- end
-
- # Holds static data from the Unicode database.
- class UnicodeDatabase
- ATTRIBUTES = :codepoints, :composition_exclusion, :composition_map, :boundary, :cp1252
-
- attr_writer(*ATTRIBUTES)
-
- def initialize
- @codepoints = Hash.new(Codepoint.new)
- @composition_exclusion = []
- @composition_map = {}
- @boundary = {}
- @cp1252 = {}
- end
-
- # Lazy load the Unicode database so it's only loaded when it's actually used
- ATTRIBUTES.each do |attr_name|
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{attr_name} # def codepoints
- load # load
- @#{attr_name} # @codepoints
- end # end
- EOS
- end
-
- # Loads the Unicode database and returns all the internal objects of
- # UnicodeDatabase.
- def load
- begin
- @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, "rb") { |f| Marshal.load f.read }
- rescue => e
- raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
- end
-
- # Redefine the === method so we can write shorter rules for grapheme cluster breaks
- @boundary.each_key do |k|
- @boundary[k].instance_eval do
- def ===(other)
- detect { |i| i === other } ? true : false
- end
- end if @boundary[k].kind_of?(Array)
- end
-
- # define attr_reader methods for the instance variables
- class << self
- attr_reader(*ATTRIBUTES)
- end
- end
-
- # Returns the directory in which the data files are stored.
- def self.dirname
- File.expand_path("../values", __dir__)
- end
-
- # Returns the filename for the data file for this version.
- def self.filename
- File.expand_path File.join(dirname, "unicode_tables.dat")
- end
+ string.swapcase
end
private
- def apply_mapping(string, mapping)
- database.codepoints
- string.each_codepoint.map do |codepoint|
- cp = database.codepoints[codepoint]
- if cp && (ncp = cp.send(mapping)) && ncp > 0
- ncp
- else
- codepoint
- end
- end.pack("U*")
- end
-
def recode_windows1252_chars(string)
string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
-
- def database
- @database ||= UnicodeDatabase.new
- end
end
end
end
diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb
index 5c34a0abb3..8b727a69ec 100644
--- a/activesupport/lib/active_support/rails.rb
+++ b/activesupport/lib/active_support/rails.rb
@@ -27,9 +27,3 @@ require "active_support/core_ext/module/delegation"
# Defines ActiveSupport::Deprecation.
require "active_support/deprecation"
-
-# Defines Regexp#match?.
-#
-# This should be removed when Rails needs Ruby 2.4 or later, and the require
-# added where other Regexp extensions are being used (easy to grep).
-require "active_support/core_ext/regexp"
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index d6dd5474d0..8ad39f7a05 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -54,25 +54,20 @@ module ActiveSupport
@@subscribers ||= []
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :subscriber, :notifier, :namespace
-
private
+ attr_reader :subscriber, :notifier, :namespace
- def add_event_subscriber(event) # :doc:
- return if %w{ start finish }.include?(event.to_s)
+ def add_event_subscriber(event) # :doc:
+ return if %w{ start finish }.include?(event.to_s)
- pattern = "#{event}.#{namespace}"
+ pattern = "#{event}.#{namespace}"
- # Don't add multiple subscribers (eg. if methods are redefined).
- return if subscriber.patterns.include?(pattern)
+ # Don't add multiple subscribers (eg. if methods are redefined).
+ return if subscriber.patterns.include?(pattern)
- subscriber.patterns << pattern
- notifier.subscribe(pattern, subscriber)
- end
+ subscriber.patterns << pattern
+ notifier.subscribe(pattern, subscriber)
+ end
end
attr_reader :patterns # :nodoc:
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index d1f7e6ea09..a698b4e61e 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -11,6 +11,7 @@ require "active_support/testing/isolation"
require "active_support/testing/constant_lookup"
require "active_support/testing/time_helpers"
require "active_support/testing/file_fixtures"
+require "active_support/testing/parallelization"
module ActiveSupport
class TestCase < ::Minitest::Test
@@ -39,6 +40,91 @@ module ActiveSupport
def test_order
ActiveSupport.test_order ||= :random
end
+
+ # Parallelizes the test suite.
+ #
+ # Takes a `workers` argument that controls how many times the process
+ # is forked. For each process a new database will be created suffixed
+ # with the worker number.
+ #
+ # test-database-0
+ # test-database-1
+ #
+ # If `ENV["PARALLEL_WORKERS"]` is set the workers argument will be ignored
+ # and the environment variable will be used instead. This is useful for CI
+ # environments, or other environments where you may need more workers than
+ # you do for local testing.
+ #
+ # If the number of workers is set to `1` or fewer, the tests will not be
+ # parallelized.
+ #
+ # The default parallelization method is to fork processes. If you'd like to
+ # use threads instead you can pass `with: :threads` to the `parallelize`
+ # method. Note the threaded parallelization does not create multiple
+ # database and will not work with system tests at this time.
+ #
+ # parallelize(workers: 2, with: :threads)
+ #
+ # The threaded parallelization uses Minitest's parallel executor directly.
+ # The processes parallelization uses a Ruby Drb server.
+ def parallelize(workers: 2, with: :processes)
+ workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
+
+ return if workers <= 1
+
+ executor = case with
+ when :processes
+ Testing::Parallelization.new(workers)
+ when :threads
+ Minitest::Parallel::Executor.new(workers)
+ else
+ raise ArgumentError, "#{with} is not a supported parallelization executor."
+ end
+
+ self.lock_threads = false if defined?(self.lock_threads) && with == :threads
+
+ Minitest.parallel_executor = executor
+
+ parallelize_me!
+ end
+
+ # Set up hook for parallel testing. This can be used if you have multiple
+ # databases or any behavior that needs to be run after the process is forked
+ # but before the tests run.
+ #
+ # Note: this feature is not available with the threaded parallelization.
+ #
+ # In your +test_helper.rb+ add the following:
+ #
+ # class ActiveSupport::TestCase
+ # parallelize_setup do
+ # # create databases
+ # end
+ # end
+ def parallelize_setup(&block)
+ ActiveSupport::Testing::Parallelization.after_fork_hook do |worker|
+ yield worker
+ end
+ end
+
+ # Clean up hook for parallel testing. This can be used to drop databases
+ # if your app uses multiple write/read databases or other clean up before
+ # the tests finish. This runs before the forked process is closed.
+ #
+ # Note: this feature is not available with the threaded parallelization.
+ #
+ # In your +test_helper.rb+ add the following:
+ #
+ # class ActiveSupport::TestCase
+ # parallelize_teardown do
+ # # drop databases
+ # end
+ # end
+ def parallelize_teardown(&block)
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do |worker|
+ yield worker
+ end
+ end
end
alias_method :method_name, :name
diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb
new file mode 100644
index 0000000000..59c8486f41
--- /dev/null
+++ b/activesupport/lib/active_support/testing/parallelization.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require "drb"
+require "drb/unix"
+
+module ActiveSupport
+ module Testing
+ class Parallelization # :nodoc:
+ class Server
+ include DRb::DRbUndumped
+
+ def initialize
+ @queue = Queue.new
+ end
+
+ def record(reporter, result)
+ reporter.synchronize do
+ reporter.record(result)
+ end
+ end
+
+ def <<(o)
+ @queue << o
+ end
+
+ def pop; @queue.pop; end
+ end
+
+ @after_fork_hooks = []
+
+ def self.after_fork_hook(&blk)
+ @after_fork_hooks << blk
+ end
+
+ def self.after_fork_hooks
+ @after_fork_hooks
+ end
+
+ @run_cleanup_hooks = []
+
+ def self.run_cleanup_hook(&blk)
+ @run_cleanup_hooks << blk
+ end
+
+ def self.run_cleanup_hooks
+ @run_cleanup_hooks
+ end
+
+ def initialize(queue_size)
+ @queue_size = queue_size
+ @queue = Server.new
+ @pool = []
+
+ @url = DRb.start_service("drbunix:", @queue).uri
+ end
+
+ def after_fork(worker)
+ self.class.after_fork_hooks.each do |cb|
+ cb.call(worker)
+ end
+ end
+
+ def run_cleanup(worker)
+ self.class.run_cleanup_hooks.each do |cb|
+ cb.call(worker)
+ end
+ end
+
+ def start
+ @pool = @queue_size.times.map do |worker|
+ fork do
+ DRb.stop_service
+
+ after_fork(worker)
+
+ queue = DRbObject.new_with_uri(@url)
+
+ while job = queue.pop
+ klass = job[0]
+ method = job[1]
+ reporter = job[2]
+ result = Minitest.run_one_method(klass, method)
+
+ queue.record(reporter, result)
+ end
+
+ run_cleanup(worker)
+ end
+ end
+ end
+
+ def <<(work)
+ @queue << work
+ end
+
+ def shutdown
+ @queue_size.times { @queue << nil }
+ @pool.each { |pid| Process.waitpid pid }
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
index 998a51a34c..801ea2909b 100644
--- a/activesupport/lib/active_support/testing/time_helpers.rb
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/module/redefine_method"
-require "active_support/core_ext/string/strip" # for strip_heredoc
require "active_support/core_ext/time/calculations"
require "concurrent/map"
@@ -112,7 +111,7 @@ module ActiveSupport
# Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
def travel_to(date_or_time)
if block_given? && simple_stubs.stubbing(Time, :now)
- travel_to_nested_block_call = <<-MSG.strip_heredoc
+ travel_to_nested_block_call = <<~MSG
Calling `travel_to` with a block, when we have previously already made a call to `travel_to`, can lead to confusing time stubbing.
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index b75f5733b5..9dfaddb825 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -2,7 +2,6 @@
require "tzinfo"
require "concurrent/map"
-require "active_support/core_ext/object/blank"
module ActiveSupport
# The TimeZone class serves as a wrapper around TZInfo::Timezone instances.
@@ -268,11 +267,14 @@ module ActiveSupport
country = TZInfo::Country.get(code)
country.zone_identifiers.map do |tz_id|
if MAPPING.value?(tz_id)
- self[MAPPING.key(tz_id)]
+ MAPPING.inject([]) do |memo, (key, value)|
+ memo << self[key] if value == tz_id
+ memo
+ end
else
create(tz_id, nil, TZInfo::Timezone.new(tz_id))
end
- end.sort!
+ end.flatten(1).sort!
end
def zones_map
diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat
deleted file mode 100644
index f7d9c48bbe..0000000000
--- a/activesupport/lib/active_support/values/unicode_tables.dat
+++ /dev/null
Binary files differ
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index ee2048cb54..e42eee07a3 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -48,10 +48,6 @@ module ActiveSupport
"Array" => "array",
"Hash" => "hash"
}
-
- # No need to map these on Ruby 2.4+
- TYPE_NAMES["Fixnum"] = "integer" unless 0.class == Integer
- TYPE_NAMES["Bignum"] = "integer" unless 0.class == Integer
end
FORMATTING = {
@@ -83,7 +79,7 @@ module ActiveSupport
end,
"boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) },
"string" => Proc.new { |string| string.to_s },
- "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
+ "yaml" => Proc.new { |yaml| YAML.load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
"binary" => Proc.new { |bin, entity| _parse_binary(bin, entity) },
"file" => Proc.new { |file, entity| _parse_file(file, entity) }
diff --git a/activesupport/test/cache/behaviors/connection_pool_behavior.rb b/activesupport/test/cache/behaviors/connection_pool_behavior.rb
index 0d46f88552..a0dcc8199c 100644
--- a/activesupport/test/cache/behaviors/connection_pool_behavior.rb
+++ b/activesupport/test/cache/behaviors/connection_pool_behavior.rb
@@ -6,7 +6,7 @@ module ConnectionPoolBehavior
emulating_latency do
begin
- cache = ActiveSupport::Cache.lookup_store(store, pool_size: 2, pool_timeout: 1)
+ cache = ActiveSupport::Cache.lookup_store(store, { pool_size: 2, pool_timeout: 1 }.merge(store_options))
cache.clear
threads = []
@@ -33,7 +33,7 @@ module ConnectionPoolBehavior
def test_no_connection_pool
emulating_latency do
begin
- cache = ActiveSupport::Cache.lookup_store(store)
+ cache = ActiveSupport::Cache.lookup_store(store, store_options)
cache.clear
threads = []
@@ -54,4 +54,7 @@ module ConnectionPoolBehavior
end
end
end
+
+ private
+ def store_options; {}; end
end
diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb
index f7302df4c8..363f2d1084 100644
--- a/activesupport/test/cache/behaviors/local_cache_behavior.rb
+++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb
@@ -119,6 +119,16 @@ module LocalCacheBehavior
end
end
+ def test_local_cache_of_fetch_multi
+ @cache.with_local_cache do
+ @cache.fetch_multi("foo", "bar") { |_key| true }
+ @peek.delete("foo")
+ @peek.delete("bar")
+ assert_equal true, @cache.read("foo")
+ assert_equal true, @cache.read("bar")
+ end
+ end
+
def test_middleware
app = lambda { |env|
result = @cache.write("foo", "bar")
diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb
index 9daa0b8796..375993a632 100644
--- a/activesupport/test/cache/stores/redis_cache_store_test.rb
+++ b/activesupport/test/cache/stores/redis_cache_store_test.rb
@@ -5,6 +5,24 @@ require "active_support/cache"
require "active_support/cache/redis_cache_store"
require_relative "../behaviors"
+driver_name = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis"
+driver = Object.const_get("Redis::Connection::#{driver_name.camelize}")
+
+Redis::Connection.drivers.clear
+Redis::Connection.drivers.append(driver)
+
+# Emulates a latency on Redis's back-end for the key latency to facilitate
+# connection pool testing.
+class SlowRedis < Redis
+ def get(key, options = {})
+ if key =~ /latency/
+ sleep 3
+ else
+ super
+ end
+ end
+end
+
module ActiveSupport::Cache::RedisCacheStoreTests
DRIVER = %w[ ruby hiredis ].include?(ENV["REDIS_DRIVER"]) ? ENV["REDIS_DRIVER"] : "hiredis"
@@ -119,6 +137,33 @@ module ActiveSupport::Cache::RedisCacheStoreTests
end
end
+ class ConnectionPoolBehaviourTest < StoreTest
+ include ConnectionPoolBehavior
+
+ private
+
+ def store
+ :redis_cache_store
+ end
+
+ def emulating_latency
+ old_redis = Object.send(:remove_const, :Redis)
+ Object.const_set(:Redis, SlowRedis)
+
+ yield
+ ensure
+ Object.send(:remove_const, :Redis)
+ Object.const_set(:Redis, old_redis)
+ end
+ end
+
+ class RedisDistributedConnectionPoolBehaviourTest < ConnectionPoolBehaviourTest
+ private
+ def store_options
+ { url: %w[ redis://localhost:6379/0 redis://localhost:6379/0 ] }
+ end
+ end
+
# Separate test class so we can omit the namespace which causes expected,
# appropriate complaints about incompatible string encodings.
class KeyEncodingSafetyTest < StoreTest
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index 30f1632460..6c7643ed72 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -482,10 +482,9 @@ module CallbacksTest
"block in run_callbacks",
"tweedle_dum",
"block in run_callbacks",
- ("call" if RUBY_VERSION < "2.3"),
"run_callbacks",
"save"
- ].compact, call_stack.map(&:label)
+ ], call_stack.map(&:label)
end
def test_short_call_stack
diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb
index c182b91826..37111a5d7d 100644
--- a/activesupport/test/core_ext/array/grouping_test.rb
+++ b/activesupport/test/core_ext/array/grouping_test.rb
@@ -4,15 +4,6 @@ require "abstract_unit"
require "active_support/core_ext/array"
class GroupingTest < ActiveSupport::TestCase
- def setup
- # In Ruby < 2.4, test we avoid Integer#/ (redefined by mathn)
- Fixnum.send :private, :/ unless 0.class == Integer
- end
-
- def teardown
- Fixnum.send :public, :/ unless 0.class == Integer
- end
-
def test_in_groups_of_with_perfect_fit
groups = []
("a".."i").to_a.in_groups_of(3) do |group|
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index be33324f25..8f6befe809 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -5,6 +5,7 @@ require "active_support/inflector"
require "active_support/time"
require "active_support/json"
require "time_zone_test_helpers"
+require "yaml"
class DurationTest < ActiveSupport::TestCase
include TimeZoneTestHelpers
@@ -23,7 +24,7 @@ class DurationTest < ActiveSupport::TestCase
end
def test_instance_of
- assert 1.minute.instance_of?(1.class)
+ assert 1.minute.instance_of?(Integer)
assert 2.days.instance_of?(ActiveSupport::Duration)
assert !3.second.instance_of?(Numeric)
end
diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb
index d34b7fa7b9..e481b5e4a9 100644
--- a/activesupport/test/core_ext/hash/transform_values_test.rb
+++ b/activesupport/test/core_ext/hash/transform_values_test.rb
@@ -2,25 +2,16 @@
require "abstract_unit"
require "active_support/core_ext/hash/indifferent_access"
-require "active_support/core_ext/hash/transform_values"
-class TransformValuesTest < ActiveSupport::TestCase
- test "transform_values returns a new hash with the values computed from the block" do
- original = { a: "a", b: "b" }
- mapped = original.transform_values { |v| v + "!" }
-
- assert_equal({ a: "a", b: "b" }, original)
- assert_equal({ a: "a!", b: "b!" }, mapped)
- end
-
- test "transform_values! modifies the values of the original" do
- original = { a: "a", b: "b" }
- mapped = original.transform_values! { |v| v + "!" }
-
- assert_equal({ a: "a!", b: "b!" }, original)
- assert_same original, mapped
+class TransformValuesDeprecatedRequireTest < ActiveSupport::TestCase
+ test "requiring transform_values is deprecated" do
+ assert_deprecated do
+ require "active_support/core_ext/hash/transform_values"
+ end
end
+end
+class IndifferentTransformValuesTest < ActiveSupport::TestCase
test "indifferent access is still indifferent after mapping values" do
original = { a: "a", b: "b" }.with_indifferent_access
mapped = original.transform_values { |v| v + "!" }
@@ -28,50 +19,4 @@ class TransformValuesTest < ActiveSupport::TestCase
assert_equal "a!", mapped[:a]
assert_equal "a!", mapped["a"]
end
-
- # This is to be consistent with the behavior of Ruby's built in methods
- # (e.g. #select, #reject) as of 2.2
- test "default values do not persist during mapping" do
- original = Hash.new("foo")
- original[:a] = "a"
- mapped = original.transform_values { |v| v + "!" }
-
- assert_equal "a!", mapped[:a]
- assert_nil mapped[:b]
- end
-
- test "default procs do not persist after mapping" do
- original = Hash.new { "foo" }
- original[:a] = "a"
- mapped = original.transform_values { |v| v + "!" }
-
- assert_equal "a!", mapped[:a]
- assert_nil mapped[:b]
- end
-
- test "transform_values returns a sized Enumerator if no block is given" do
- original = { a: "a", b: "b" }
- enumerator = original.transform_values
- assert_equal original.size, enumerator.size
- assert_equal Enumerator, enumerator.class
- end
-
- test "transform_values! returns a sized Enumerator if no block is given" do
- original = { a: "a", b: "b" }
- enumerator = original.transform_values!
- assert_equal original.size, enumerator.size
- assert_equal Enumerator, enumerator.class
- end
-
- test "transform_values is chainable with Enumerable methods" do
- original = { a: "a", b: "b" }
- mapped = original.transform_values.with_index { |v, i| [v, i].join }
- assert_equal({ a: "a0", b: "b1" }, mapped)
- end
-
- test "transform_values! is chainable with Enumerable methods" do
- original = { a: "a", b: "b" }
- original.transform_values!.with_index { |v, i| [v, i].join }
- assert_equal({ a: "a0", b: "b1" }, original)
- end
end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index ebbe9c304c..36cfd885d7 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -440,4 +440,61 @@ class ModuleTest < ActiveSupport::TestCase
assert_not_respond_to place, :the_city
assert place.respond_to?(:the_city, true)
end
+
+ def test_private_delegate_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
+ end
+
+ delegate(:street, :city, to: :@place, private: true)
+ end
+
+ place = location.new(Somewhere.new("Such street", "Sad city"))
+
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
+
+ assert place.respond_to?(:street, true) # Asking for private method
+ assert place.respond_to?(:city, true)
+ end
+
+ def test_some_public_some_private_delegate_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
+ end
+
+ delegate(:street, to: :@place)
+ delegate(:city, to: :@place, private: true)
+ end
+
+ place = location.new(Somewhere.new("Such street", "Sad city"))
+
+ assert_respond_to place, :street
+ assert_not_respond_to place, :city
+
+ assert place.respond_to?(:city, true) # Asking for private method
+ end
+
+ def test_private_delegate_prefixed_with_private_option
+ location = Class.new do
+ def initialize(place)
+ @place = place
+ end
+ end
+
+ assert_equal %i(the_street the_city),
+ location.delegate(:street, :city, to: :@place, prefix: :the, private: true)
+
+ place = location.new(Somewhere.new("Such street", "Sad city"))
+
+ assert_not_respond_to place, :street
+ assert_not_respond_to place, :city
+
+ assert_not_respond_to place, :the_street
+ assert place.respond_to?(:the_street, true)
+ assert_not_respond_to place, :the_city
+ assert place.respond_to?(:the_city, true)
+ end
end
diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb
index b984becce3..635dd7f281 100644
--- a/activesupport/test/core_ext/object/duplicable_test.rb
+++ b/activesupport/test/core_ext/object/duplicable_test.rb
@@ -9,15 +9,9 @@ class DuplicableTest < ActiveSupport::TestCase
if RUBY_VERSION >= "2.5.0"
RAISE_DUP = [method(:puts)]
ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3, Complex(1), Rational(1)]
- elsif RUBY_VERSION >= "2.4.1"
+ else
RAISE_DUP = [method(:puts), Complex(1), Rational(1)]
ALLOW_DUP = ["1", "symbol_from_string".to_sym, Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3]
- elsif RUBY_VERSION >= "2.4.0" # Due to 2.4.0 bug. This elsif cannot be removed unless we drop 2.4.0 support...
- RAISE_DUP = [method(:puts), Complex(1), Rational(1), "symbol_from_string".to_sym]
- ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56"), nil, false, true, 1, 2.3]
- else
- RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts), Complex(1), Rational(1)]
- ALLOW_DUP = ["1", Object.new, /foo/, [], {}, Time.now, Class.new, Module.new, BigDecimal("4.56")]
end
def test_duplicable
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 472190277e..ccaa5dc786 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -9,9 +9,9 @@ require "constantize_test_cases"
require "active_support/inflector"
require "active_support/core_ext/string"
require "active_support/time"
-require "active_support/core_ext/string/strip"
require "active_support/core_ext/string/output_safety"
require "active_support/core_ext/string/indent"
+require "active_support/core_ext/string/strip"
require "time_zone_test_helpers"
require "yaml"
@@ -24,6 +24,10 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal "", "".strip_heredoc
end
+ def test_strip_heredoc_on_a_frozen_string
+ assert "".freeze.strip_heredoc.frozen?
+ end
+
def test_strip_heredoc_on_a_string_with_no_lines
assert_equal "x", "x".strip_heredoc
assert_equal "x", " x".strip_heredoc
@@ -281,6 +285,68 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, omission: "[...]", separator: /\s/)
end
+ def test_truncate_bytes
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: nil)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: " ")
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: "🖖")
+
+ assert_equal "👍👍👍…", "👍👍👍👍".truncate_bytes(15)
+ assert_equal "👍👍👍", "👍👍👍👍".truncate_bytes(15, omission: nil)
+ assert_equal "👍👍👍 ", "👍👍👍👍".truncate_bytes(15, omission: " ")
+ assert_equal "👍👍🖖", "👍👍👍👍".truncate_bytes(15, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(5)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(5, omission: nil)
+ assert_equal "👍 ", "👍👍👍👍".truncate_bytes(5, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(5, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(4)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(4, omission: nil)
+ assert_equal " ", "👍👍👍👍".truncate_bytes(4, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(4, omission: "🖖")
+
+ assert_raise ArgumentError do
+ "👍👍👍👍".truncate_bytes(3, omission: "🖖")
+ end
+ end
+
+ def test_truncate_bytes_preserves_codepoints
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: nil)
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: " ")
+ assert_equal "👍👍👍👍", "👍👍👍👍".truncate_bytes(16, omission: "🖖")
+
+ assert_equal "👍👍👍…", "👍👍👍👍".truncate_bytes(15)
+ assert_equal "👍👍👍", "👍👍👍👍".truncate_bytes(15, omission: nil)
+ assert_equal "👍👍👍 ", "👍👍👍👍".truncate_bytes(15, omission: " ")
+ assert_equal "👍👍🖖", "👍👍👍👍".truncate_bytes(15, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(5)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(5, omission: nil)
+ assert_equal "👍 ", "👍👍👍👍".truncate_bytes(5, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(5, omission: "🖖")
+
+ assert_equal "…", "👍👍👍👍".truncate_bytes(4)
+ assert_equal "👍", "👍👍👍👍".truncate_bytes(4, omission: nil)
+ assert_equal " ", "👍👍👍👍".truncate_bytes(4, omission: " ")
+ assert_equal "🖖", "👍👍👍👍".truncate_bytes(4, omission: "🖖")
+
+ assert_raise ArgumentError do
+ "👍👍👍👍".truncate_bytes(3, omission: "🖖")
+ end
+ end
+
+ def test_truncates_bytes_preserves_grapheme_clusters
+ assert_equal "a ", "a ❤️ b".truncate_bytes(2, omission: nil)
+ assert_equal "a ", "a ❤️ b".truncate_bytes(3, omission: nil)
+ assert_equal "a ", "a ❤️ b".truncate_bytes(7, omission: nil)
+ assert_equal "a ❤️", "a ❤️ b".truncate_bytes(8, omission: nil)
+
+ assert_equal "a ", "a 👩‍❤️‍👩".truncate_bytes(13, omission: nil)
+ assert_equal "", "👩‍❤️‍👩".truncate_bytes(13, omission: nil)
+ end
+
def test_truncate_words
assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3)
assert_equal "Hello Big...", "Hello Big World!".truncate_words(2)
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index be3b8a49a9..2ea5f0921c 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -3,7 +3,6 @@
require "abstract_unit"
require "active_support/time"
require "time_zone_test_helpers"
-require "active_support/core_ext/string/strip"
require "yaml"
class TimeWithZoneTest < ActiveSupport::TestCase
@@ -163,7 +162,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_to_yaml
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
--- !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
zone: !ruby/object:ActiveSupport::TimeZone
@@ -175,7 +174,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_ruby_to_yaml
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
---
twz: !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
@@ -188,7 +187,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_yaml_load
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
--- !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
zone: !ruby/object:ActiveSupport::TimeZone
@@ -200,7 +199,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_ruby_yaml_load
- yaml = <<-EOF.strip_heredoc
+ yaml = <<~EOF
---
twz: !ruby/object:ActiveSupport::TimeWithZone
utc: 2000-01-01 00:00:00.000000000 Z
diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb
index 41d653fa59..b06250baf8 100644
--- a/activesupport/test/hash_with_indifferent_access_test.rb
+++ b/activesupport/test/hash_with_indifferent_access_test.rb
@@ -169,8 +169,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
end
def test_indifferent_fetch_values
- skip unless Hash.method_defined?(:fetch_values)
-
@mixed = @mixed.with_indifferent_access
assert_equal [1, 2], @mixed.fetch_values("a", "b")
@@ -404,6 +402,12 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
assert_equal({ "aa" => 1, "bb" => 2 }, hash)
assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).transform_keys { |k| k.to_sym }
+
+ assert_equal(1, hash[:a])
+ assert_equal(1, hash["a"])
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
end
def test_indifferent_transform_keys_bang
@@ -412,6 +416,13 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
assert_equal({ "aa" => 1, "bb" => 2 }, indifferent_strings)
assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.transform_keys! { |k| k.to_sym }
+
+ assert_equal(1, indifferent_strings[:a])
+ assert_equal(1, indifferent_strings["a"])
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
end
def test_indifferent_transform_values
@@ -562,7 +573,6 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
end
def test_nested_dig_indifferent_access
- skip if RUBY_VERSION < "2.3.0"
data = { "this" => { "views" => 1234 } }.with_indifferent_access
assert_equal 1234, data.dig(:this, :views)
end
diff --git a/activesupport/test/multibyte_unicode_database_test.rb b/activesupport/test/multibyte_unicode_database_test.rb
deleted file mode 100644
index 540a34493d..0000000000
--- a/activesupport/test/multibyte_unicode_database_test.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-# frozen_string_literal: true
-
-require "abstract_unit"
-
-class MultibyteUnicodeDatabaseTest < ActiveSupport::TestCase
- include ActiveSupport::Multibyte::Unicode
-
- def setup
- @ucd = UnicodeDatabase.new
- end
-
- UnicodeDatabase::ATTRIBUTES.each do |attribute|
- define_method "test_lazy_loading_on_attribute_access_of_#{attribute}" do
- assert_called(@ucd, :load) do
- @ucd.send(attribute)
- end
- end
- end
-
- def test_load
- @ucd.load
- UnicodeDatabase::ATTRIBUTES.each do |attribute|
- assert @ucd.send(attribute).length > 1
- end
- end
-end
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index 405c8f315b..63ca22efb5 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -756,6 +756,16 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_not_includes ActiveSupport::TimeZone.country_zones(:ru), ActiveSupport::TimeZone["Kuala Lumpur"]
end
+ def test_country_zones_with_and_without_mappings
+ assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Adelaide"]
+ assert_includes ActiveSupport::TimeZone.country_zones("au"), ActiveSupport::TimeZone["Australia/Lord_Howe"]
+ end
+
+ def test_country_zones_with_multiple_mappings
+ assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["Edinburgh"]
+ assert_includes ActiveSupport::TimeZone.country_zones("gb"), ActiveSupport::TimeZone["London"]
+ end
+
def test_country_zones_without_mappings
assert_includes ActiveSupport::TimeZone.country_zones(:sv), ActiveSupport::TimeZone["America/El_Salvador"]
end
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index 64e8b51817..21b4ed4b9f 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,3 +1,8 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/guides/CHANGELOG.md) for previous changes.
diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb
index 557b1d7bef..7fc85e636a 100644
--- a/guides/bug_report_templates/action_controller_gem.rb
+++ b/guides/bug_report_templates/action_controller_gem.rb
@@ -13,7 +13,7 @@ gemfile(true) do
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
- gem "rails", "5.1.0"
+ gem "rails", "5.2.0.rc1"
end
require "rack/test"
diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb
index 013d1f8602..6b30a7d446 100644
--- a/guides/bug_report_templates/active_job_gem.rb
+++ b/guides/bug_report_templates/active_job_gem.rb
@@ -13,7 +13,7 @@ gemfile(true) do
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
- gem "activejob", "5.1.0"
+ gem "activejob", "5.2.0.rc1"
end
require "minitest/autorun"
diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb
index 921917fbe9..fabc2a2382 100644
--- a/guides/bug_report_templates/active_record_gem.rb
+++ b/guides/bug_report_templates/active_record_gem.rb
@@ -13,7 +13,7 @@ gemfile(true) do
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
- gem "activerecord", "5.1.0"
+ gem "activerecord", "5.2.0.rc1"
gem "sqlite3"
end
diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb
index 9002b8f428..ca9987f956 100644
--- a/guides/bug_report_templates/active_record_migrations_gem.rb
+++ b/guides/bug_report_templates/active_record_migrations_gem.rb
@@ -13,7 +13,7 @@ gemfile(true) do
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
- gem "activerecord", "5.1.0"
+ gem "activerecord", "5.2.0.rc1"
gem "sqlite3"
end
@@ -37,7 +37,7 @@ end
class Payment < ActiveRecord::Base
end
-class ChangeAmountToAddScale < ActiveRecord::Migration[5.1]
+class ChangeAmountToAddScale < ActiveRecord::Migration[5.2]
def change
reversible do |dir|
dir.up do
diff --git a/guides/bug_report_templates/active_record_migrations_master.rb b/guides/bug_report_templates/active_record_migrations_master.rb
index 194b093ac3..715dca98ba 100644
--- a/guides/bug_report_templates/active_record_migrations_master.rb
+++ b/guides/bug_report_templates/active_record_migrations_master.rb
@@ -36,7 +36,7 @@ end
class Payment < ActiveRecord::Base
end
-class ChangeAmountToAddScale < ActiveRecord::Migration[5.2]
+class ChangeAmountToAddScale < ActiveRecord::Migration[6.0]
def change
reversible do |dir|
dir.up do
diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb
index 60e8322c2a..7a55d7c660 100644
--- a/guides/bug_report_templates/generic_gem.rb
+++ b/guides/bug_report_templates/generic_gem.rb
@@ -13,9 +13,10 @@ gemfile(true) do
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
# Activate the gem you are reporting the issue against.
- gem "activesupport", "5.1.0"
+ gem "activesupport", "5.2.0.rc1"
end
+require "active_support"
require "active_support/core_ext/object/blank"
require "minitest/autorun"
diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb
index 87a369a15a..5c4f7d159c 100644
--- a/guides/rails_guides/kindle.rb
+++ b/guides/rails_guides/kindle.rb
@@ -58,9 +58,9 @@ module Kindle
end
def generate_sections(html_pages)
- FileUtils::rm_rf("sections/")
+ FileUtils.rm_rf("sections/")
html_pages.each_with_index do |page, section_idx|
- FileUtils::mkdir_p("sections/%03d" % section_idx)
+ FileUtils.mkdir_p("sections/%03d" % section_idx)
doc = Nokogiri::HTML(File.open(page))
title = doc.at("title").inner_text.gsub("Ruby on Rails Guides: ", "")
title = page.capitalize.gsub(".html", "") if title.strip == ""
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
index 914ef2c327..97d98efba0 100644
--- a/guides/source/active_job_basics.md
+++ b/guides/source/active_job_basics.md
@@ -276,7 +276,7 @@ class GuestsCleanupJob < ApplicationJob
end
private
- def around_cleanup(job)
+ def around_cleanup
# Do something before perform
yield
# Do something after perform
@@ -339,8 +339,23 @@ UserMailer.welcome(@user).deliver_later # Email will be localized to Esperanto.
```
-GlobalID
---------
+Supported types for arguments
+----------------------------
+
+ActiveJob supports the following types of arguments by default:
+
+ - Basic types (`NilClass`, `String`, `Integer`, `Float`, `BigDecimal`, `TrueClass`, `FalseClass`)
+ - `Symbol`
+ - `Date`
+ - `Time`
+ - `DateTime`
+ - `ActiveSupport::TimeWithZone`
+ - `ActiveSupport::Duration`
+ - `Hash` (Keys should be of `String` or `Symbol` type)
+ - `ActiveSupport::HashWithIndifferentAccess`
+ - `Array`
+
+### GlobalID
Active Job supports GlobalID for parameters. This makes it possible to pass live
Active Record objects to your job instead of class/id pairs, which you then have
@@ -368,6 +383,39 @@ end
This works with any class that mixes in `GlobalID::Identification`, which
by default has been mixed into Active Record classes.
+### Serializers
+
+You can extend the list of supported argument types. You just need to define your own serializer:
+
+```ruby
+class MoneySerializer < ActiveJob::Serializers::ObjectSerializer
+ # Checks if an argument should be serialized by this serializer.
+ def serialize?(argument)
+ argument.is_a? Money
+ end
+
+ # Converts an object to a simpler representative using supported object types.
+ # The recommended representative is a Hash with a specific key. Keys can be of basic types only.
+ # You should call `super` to add the custom serializer type to the hash.
+ def serialize(money)
+ super(
+ "amount" => money.amount,
+ "currency" => money.currency
+ )
+ end
+
+ # Converts serialized value into a proper object.
+ def deserialize(hash)
+ Money.new(hash["amount"], hash["currency"])
+ end
+end
+```
+
+and add this serializer to the list:
+
+```ruby
+Rails.application.config.active_job.custom_serializers << MoneySerializer
+```
Exceptions
----------
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
index 630dafe632..4f54b4c206 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -264,7 +264,7 @@ The whole callback chain is wrapped in a transaction. If any callback raises an
throw :abort
```
-WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception.
+WARNING. Any exception that is not `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` or `ActiveRecord::RecordInvalid` may break code that does not expect methods like `save` and `update` (which normally try to return `true` or `false`) to raise an exception.
Relational Callbacks
--------------------
diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md
index 97c56dfd93..a7cb14c52a 100644
--- a/guides/source/active_storage_overview.md
+++ b/guides/source/active_storage_overview.md
@@ -121,6 +121,8 @@ Add the [`aws-sdk-s3`](https://github.com/aws/aws-sdk-ruby) gem to your `Gemfile
gem "aws-sdk-s3", require: false
```
+NOTE: The core features of Active Storage require the following permissions: `s3:ListBucket`, `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject`. If you have additional upload options configured such as setting ACLs then additional permissions may be required.
+
### Microsoft Azure Storage Service
Declare an Azure Storage service in `config/storage.yml`:
@@ -556,6 +558,30 @@ config.active_job.queue_adapter = :inline
config.active_storage.service = :local_test
```
+Discarding Files Stored During Integration Tests
+-------------------------------------------
+
+Similarly to System Tests, files uploaded during Integration Tests will not be
+automatically cleaned up. If you want to clear the files, you can do it in an
+`after_teardown` callback. Doing it here ensures that all connections created
+during the test are complete and you won't receive an error from Active Storage
+saying it can't find a file.
+
+```ruby
+module ActionDispatch
+ class IntegrationTest
+ def remove_uploaded_files
+ FileUtils.rm_rf(Rails.root.join('tmp', 'storage'))
+ end
+
+ def after_teardown
+ super
+ remove_uploaded_files
+ end
+ end
+end
+```
+
Implementing Support for Other Cloud Services
---------------------------------------------
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 8e2826bb85..73b24b900a 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -135,16 +135,14 @@ NOTE: Defined in `active_support/core_ext/object/blank.rb`.
### `duplicable?`
-In Ruby 2.4 most objects can be duplicated via `dup` or `clone` except
-methods and certain numbers. Though Ruby 2.2 and 2.3 can't duplicate `nil`,
-`false`, `true`, and symbols as well as instances `Float`, `Fixnum`,
-and `Bignum` instances.
+As of Ruby 2.5, most objects can be duplicated via `dup` or `clone`:
```ruby
"foo".dup # => "foo"
"".dup # => ""
-1.method(:+).dup # => TypeError: allocator undefined for Method
-Complex(0).dup # => TypeError: can't copy Complex
+Rational(1).dup # => (1/1)
+Complex(0).dup # => (0+0i)
+1.method(:+).dup # => TypeError (allocator undefined for Method)
```
Active Support provides `duplicable?` to query an object about this:
@@ -152,35 +150,18 @@ Active Support provides `duplicable?` to query an object about this:
```ruby
"foo".duplicable? # => true
"".duplicable? # => true
-Rational(1).duplicable? # => false
-Complex(1).duplicable? # => false
+Rational(1).duplicable? # => true
+Complex(1).duplicable? # => true
1.method(:+).duplicable? # => false
```
-`duplicable?` matches Ruby's `dup` according to the Ruby version.
-
-So in 2.4:
+`duplicable?` matches the current Ruby version's `dup` behavior,
+so results will vary according the version of Ruby you're using.
+In Ruby 2.4, for example, Complex and Rational are not duplicable:
```ruby
-nil.dup # => nil
-:my_symbol.dup # => :my_symbol
-1.dup # => 1
-
-nil.duplicable? # => true
-:my_symbol.duplicable? # => true
-1.duplicable? # => true
-```
-
-Whereas in 2.2 and 2.3:
-
-```ruby
-nil.dup # => TypeError: can't dup NilClass
-:my_symbol.dup # => TypeError: can't dup Symbol
-1.dup # => TypeError: can't dup Fixnum
-
-nil.duplicable? # => false
-:my_symbol.duplicable? # => false
-1.duplicable? # => false
+Rational(1).duplicable? # => false
+Complex(1).duplicable? # => false
```
WARNING: Any class can disallow duplication by removing `dup` and `clone` or raising exceptions from them. Thus only `rescue` can tell whether a given arbitrary object is duplicable. `duplicable?` depends on the hard-coded list above, but it is much faster than `rescue`. Use it only if you know the hard-coded list is enough in your use case.
@@ -925,6 +906,15 @@ The macros `cattr_reader`, `cattr_writer`, and `cattr_accessor` are analogous to
```ruby
class MysqlAdapter < AbstractAdapter
# Generates class methods to access @@emulate_booleans.
+ cattr_accessor :emulate_booleans
+end
+```
+
+Also, you can pass a block to `cattr_*` to set up the attribute with a default value:
+
+```ruby
+class MysqlAdapter < AbstractAdapter
+ # Generates class methods to access @@emulate_booleans with default value of true.
cattr_accessor :emulate_booleans, default: true
end
```
@@ -941,15 +931,6 @@ end
we can access `field_error_proc` in views.
-Also, you can pass a block to `cattr_*` to set up the attribute with a default value:
-
-```ruby
-class MysqlAdapter < AbstractAdapter
- # Generates class methods to access @@emulate_booleans with default value of true.
- cattr_accessor :emulate_booleans, default: true
-end
-```
-
The generation of the reader instance method can be prevented by setting `:instance_reader` to `false` and the generation of the writer instance method can be prevented by setting `:instance_writer` to `false`. Generation of both methods can be prevented by setting `:instance_accessor` to `false`. In all cases, the value must be exactly `false` and not any false value.
```ruby
@@ -2776,20 +2757,6 @@ Active Record does not accept unknown options when building associations, for ex
NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
-### Working with Values
-
-#### `transform_values` && `transform_values!`
-
-The method `transform_values` accepts a block and returns a hash that has applied the block operations to each of the values in the receiver.
-
-```ruby
-{ nil => nil, 1 => 1, :x => :a }.transform_values { |value| value.to_s.upcase }
-# => {nil=>"", 1=>"1", :x=>"A"}
-```
-There's also the bang variant `transform_values!` that applies the block operations to values in the very receiver.
-
-NOTE: Defined in `active_support/core_ext/hash/transform_values.rb`.
-
### Slicing
Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes:
@@ -2890,24 +2857,6 @@ end
NOTE: Defined in `active_support/core_ext/regexp.rb`.
-### `match?`
-
-Rails implements `Regexp#match?` for Ruby versions prior to 2.4:
-
-```ruby
-/oo/.match?('foo') # => true
-/oo/.match?('bar') # => false
-/oo/.match?('foo', 1) # => true
-```
-
-The backport has the same interface and lack of side-effects in the caller like
-not setting `$1` and friends, but it does not have the speed benefits. Its
-purpose is to be able to write 2.4 compatible code. Rails itself uses this
-predicate internally for example.
-
-Active Support defines `Regexp#match?` only if not present, so code running
-under 2.4 or later does run the original one and gets the performance boost.
-
Extensions to `Range`
---------------------
diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md
index 6d30910ce8..5428b16edc 100644
--- a/guides/source/autoloading_and_reloading_constants.md
+++ b/guides/source/autoloading_and_reloading_constants.md
@@ -476,7 +476,7 @@ How files are autoloaded depends on `eager_load` and `cache_classes` config sett
What is described above are the defaults with a newly generated Rails app. There are multiple ways this can be configured differently (see [Configuring Rails Applications](configuring.html#rails-general-configuration).
). But using `autoload_paths` on its own in the past (before Rails 5) developers might configure `autoload_paths` to add in extra locations (e.g. `lib` which used to be an autoload path list years ago, but no longer is). However this is now discouraged for most purposes, as it is likely to lead to production-only errors. It is possible to add new locations to both `config.eager_load_paths` and `config.autoload_paths` but use at your own risk.
-See also ([Autoloading in the Test Environment](#autoloading-in-the-test-environment).
+See also [Autoloading in the Test Environment](#autoloading-in-the-test-environment).
`config.autoload_paths` is not changeable from environment-specific configuration files.
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index cd9f4b4a68..5dde6f34fa 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -465,6 +465,10 @@ is often faster than waiting more than a second to retrieve it. Both read and
write timeouts default to 1 second, but may be set lower if your network is
consistently low latency.
+By default, the cache store will not attempt to reconnect to Redis if the
+connection fails during a request. If you experience frequent disconnects you
+may wish to enable reconnect attempts.
+
Cache reads and writes never raise exceptions. They just return `nil` instead,
behaving as if there was nothing in the cache. To gauge whether your cache is
hitting exceptions, you may provide an `error_handler` to report to an
@@ -480,9 +484,10 @@ like this:
cache_servers = %w[ "redis://cache-01:6379/0", "redis://cache-02:6379/0", … ],
config.cache_store = :redis_cache_store, url: cache_servers,
- connect_timeout: 30, # Defaults to 20 seconds
- read_timeout: 0.2, # Defaults to 1 second
- write_timeout: 0.2, # Defaults to 1 second
+ connect_timeout: 30, # Defaults to 20 seconds
+ read_timeout: 0.2, # Defaults to 1 second
+ write_timeout: 0.2, # Defaults to 1 second
+ reconnect_attempts: 1, # Defaults to 0
error_handler: -> (method:, returning:, exception:) {
# Report errors to Sentry as warnings
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 648645af7c..b41e8bbec6 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -431,16 +431,16 @@ INFO: You can also use `bin/rails -T` to get the list of tasks.
```bash
$ bin/rails about
About your application's environment
-Rails version 5.1.0
-Ruby version 2.2.2 (x86_64-linux)
-RubyGems version 2.4.6
-Rack version 2.0.1
+Rails version 6.0.0
+Ruby version 2.5.0 (x86_64-linux)
+RubyGems version 2.7.3
+Rack version 2.0.4
JavaScript Runtime Node.js (V8)
Middleware: Rack::Sendfile, ActionDispatch::Static, ActionDispatch::Executor, ActiveSupport::Cache::Strategy::LocalCache::Middleware, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, ActionDispatch::RemoteIp, Sprockets::Rails::QuietAssets, Rails::Rack::Logger, ActionDispatch::ShowExceptions, WebConsole::Middleware, ActionDispatch::DebugExceptions, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, Rack::Head, Rack::ConditionalGet, Rack::ETag
Application root /home/foobar/commandsapp
Environment development
Database adapter sqlite3
-Database schema version 20110805173523
+Database schema version 20180205173523
```
### `assets`
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index a0bf6046da..a87b8a2f48 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -588,6 +588,8 @@ Defaults to `'signed cookie'`.
* `config.action_view.form_with_generates_ids` determines whether `form_with` generates ids on inputs. This defaults to `true`.
+* `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`.
+
### Configuring Action Mailer
There are a number of settings available on `config.action_mailer`:
@@ -741,6 +743,8 @@ There are a few configuration options available in Active Support:
* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging.
+* `config.active_job.custom_serializers` allows to set custom argument serializers. Defaults to `[]`.
+
### Configuring Action Cable
* `config.action_cable.url` accepts a string for the URL for where
@@ -752,6 +756,41 @@ main application.
You can set this as nil to not mount Action Cable as part of your
normal Rails server.
+
+### Configuring Active Storage
+
+`config.active_storage` provides the following configuration options:
+
+* `config.active_storage.analyzers` accepts an array of classes indicating the analyzers available for Active Storage blobs. The default is `[ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer]`. The former can extract width and height of an image blob; the latter can extract width, height, duration, angle, and aspect ratio of a video blob.
+
+* `config.active_storage.previewers` accepts an array of classes indicating the image previewers available in Active Storage blobs. The default is `[ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer]`. The former can generate a thumbnail from the first page of a PDF blob; the latter from the relevant frame of a video blob.
+
+* `config.active_storage.paths` accepts a hash of options indicating the locations of previewer/analyzer commands. The default is `{}`, meaning the commands will be looked for in the default path. Can include any of these options:
+ * `:ffprobe` - The location of the ffprobe executable.
+ * `:mutool` - The location of the mutool executable.
+ * `:ffmpeg` - The location of the ffmpeg executable.
+
+ ```ruby
+ config.active_storage.paths[:ffprobe] = '/usr/local/bin/ffprobe'
+ ```
+
+* `config.active_storage.variable_content_types` accepts an array of strings indicating the content types that Active Storage can transform through ImageMagick. The default is `%w(image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop)`.
+
+* `config.active_storage.content_types_to_serve_as_binary` accepts an array of strings indicating the content types that Active Storage will always serve as an attachment, rather than inline. The default is `%w(text/html
+text/javascript image/svg+xml application/postscript application/x-shockwave-flash text/xml application/xml application/xhtml+xml)`.
+
+* `config.active_storage.queue` can be used to set the name of the Active Job queue used to perform jobs like analyzing the content of a blob or purging a blog.
+
+ ```ruby
+ config.active_job.queue = :low_priority
+ ```
+
+* `config.active_storage.logger` can be used to set the logger used by Active Storage. Accepts a logger conforming to the interface of Log4r or the default Ruby Logger class.
+
+ ```ruby
+ config.active_job.logger = ActiveSupport::Logger.new(STDOUT)
+ ```
+
### Configuring a Database
Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`.
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 6cf99a7e5c..f545b90103 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -87,10 +87,10 @@ current version of Ruby installed:
```bash
$ ruby -v
-ruby 2.3.1p112
+ruby 2.5.0
```
-Rails requires Ruby version 2.2.2 or later. If the version number returned is
+Rails requires Ruby version 2.4.1 or later. If the version number returned is
less than that number, you'll need to install a fresh copy of Ruby.
TIP: A number of tools exist to help you quickly install Ruby and Ruby
@@ -100,7 +100,7 @@ For more installation methods for most Operating Systems take a look at
[ruby-lang.org](https://www.ruby-lang.org/en/documentation/installation/).
If you are working on Windows, you should also install the
-[Ruby Installer Development Kit](http://rubyinstaller.org/downloads/).
+[Ruby Installer Development Kit](https://rubyinstaller.org/downloads/).
You will also need an installation of the SQLite3 database.
Many popular UNIX-like OSes ship with an acceptable version of SQLite3.
@@ -170,7 +170,7 @@ of the files and folders that Rails created by default:
| File/Folder | Purpose |
| ----------- | ------- |
|app/|Contains the controllers, models, views, helpers, mailers, channels, jobs and assets for your application. You'll focus on this folder for the remainder of this guide.|
-|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy or run your application.|
+|bin/|Contains the rails script that starts your app and can contain other scripts you use to setup, update, deploy, or run your application.|
|config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).|
|config.ru|Rack configuration for Rack based servers used to start the application. For more information about Rack, see the [Rack website](https://rack.github.io/).|
|db/|Contains your current database schema, as well as the database migrations.|
@@ -346,9 +346,9 @@ Now that you've seen how to create a controller, an action and a view, let's
create something with a bit more substance.
In the Blog application, you will now create a new _resource_. A resource is the
-term used for a collection of similar objects, such as articles, people or
+term used for a collection of similar objects, such as articles, people, or
animals.
-You can create, read, update and destroy items for a resource and these
+You can create, read, update, and destroy items for a resource and these
operations are referred to as _CRUD_ operations.
Rails provides a `resources` method which can be used to declare a standard REST
@@ -504,7 +504,7 @@ write this content in it:
```
When you refresh <http://localhost:3000/articles/new> you'll now see that the
-page has a title. The route, controller, action and view are now working
+page has a title. The route, controller, action, and view are now working
harmoniously! It's time to create the form for a new article.
### The first form
@@ -1122,7 +1122,7 @@ that otherwise `@article` would be `nil` in our view, and calling
`@article.errors.any?` would throw an error.
TIP: Rails automatically wraps fields that contain an error with a div
-with class `field_with_errors`. You can define a css rule to make them
+with class `field_with_errors`. You can define a CSS rule to make them
standout.
Now you'll get a nice error message when saving an article without title when
@@ -1204,10 +1204,10 @@ it look as follows:
This time we point the form to the `update` action, which is not defined yet
but will be very soon.
-Passing the article object to the method, will automagically create url for submitting the edited article form.
-This option tells Rails that we want this form to be submitted
-via the `PATCH` HTTP method which is the HTTP method you're expected to use to
-**update** resources according to the REST protocol.
+Passing the article object to the method will automatically set the URL for
+submitting the edited article form. This option tells Rails that we want this
+form to be submitted via the `PATCH` HTTP method, which is the HTTP method you're
+expected to use to **update** resources according to the REST protocol.
The arguments to `form_with` could be model objects, say, `model: @article` which would
cause the helper to fill in the form with the fields of the object. Passing in a
@@ -1506,7 +1506,7 @@ appear.
TIP: Learn more about Unobtrusive JavaScript on
[Working With JavaScript in Rails](working_with_javascript_in_rails.html) guide.
-Congratulations, you can now create, show, list, update and destroy
+Congratulations, you can now create, show, list, update, and destroy
articles.
TIP: In general, Rails encourages using resources objects instead of
@@ -2065,8 +2065,8 @@ getting up and running with Rails, feel free to consult these support
resources:
* The [Ruby on Rails Guides](index.html)
-* The [Ruby on Rails Tutorial](http://railstutorial.org/book)
-* The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk)
+* The [Ruby on Rails Tutorial](https://www.railstutorial.org/book)
+* The [Ruby on Rails mailing list](https://groups.google.com/group/rubyonrails-talk)
* The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index c4f1df487b..d3b122c7fe 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -532,12 +532,12 @@ require "rails"
%w(
active_record/railtie
+ active_storage/engine
action_controller/railtie
action_view/railtie
action_mailer/railtie
active_job/railtie
action_cable/engine
- active_storage/engine
rails/test_unit/railtie
sprockets/railtie
).each do |railtie|
diff --git a/guides/source/routing.md b/guides/source/routing.md
index efc0e32b56..1e75cbf362 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -549,7 +549,7 @@ In particular, simple routing makes it very easy to map legacy URLs to new Rails
When you set up a regular route, you supply a series of symbols that Rails maps to parts of an incoming HTTP request. For example, consider this route:
```ruby
-get 'photos(/:id)', to: :display
+get 'photos(/:id)', to: 'photos#display'
```
If an incoming request of `/photos/1` is processed by this route (because it hasn't matched any previous route in the file), then the result will be to invoke the `display` action of the `PhotosController`, and to make the final parameter `"1"` available as `params[:id]`. This route will also route the incoming request of `/photos` to `PhotosController#display`, since `:id` is an optional parameter, denoted by parentheses.
diff --git a/guides/source/security.md b/guides/source/security.md
index 74256c7b84..28ddbdc26a 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -573,18 +573,6 @@ config.filter_parameters << :password
NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
-### Good Passwords
-
-INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._
-
-Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are:
-
-password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey.
-
-It is interesting that only 4% of these passwords were dictionary words and the great majority is actually alphanumeric. However, password cracker dictionaries contain a large number of today's passwords, and they try out all kinds of (alphanumerical) combinations. If an attacker knows your user name and you use a weak password, your account will be easily cracked.
-
-A good password is a long alphanumeric combination of mixed cases. As this is quite hard to remember, it is advisable to enter only the _first letters of a sentence that you can easily remember_. For example "The quick brown fox jumps over the lazy dog" will be "Tqbfjotld". Note that this is just an example, you should not use well known phrases like these, as they might appear in cracker dictionaries, too.
-
### Regular Expressions
INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._
diff --git a/guides/source/testing.md b/guides/source/testing.md
index af6127f4a5..b9b310cbba 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -462,6 +462,89 @@ Rails options:
-c, --[no-]color Enable color in the output
```
+Parallel Testing
+----------------
+
+Parallel testing allows you to parallelize your test suite. While forking processes is the
+default method, threading is supported as well. Running tests in parallel reduces the time it
+takes your entire test suite to run.
+
+### Parallel testing with processes
+
+The default parallelization method is to fork processes using Ruby's DRb system. The processes
+are forked based on the number of workers provided. The default is 2, but can be changed by the
+number passed to the parallelize method. Active Record automatically handles creating and
+migrating a new database for each worker to use.
+
+To enable parallelization add the following to your `test_helper.rb`:
+
+```
+class ActiveSupport::TestCase
+ parallelize(workers: 2)
+end
+```
+
+The number of workers passed is the number of times the process will be forked. You may want to
+parallelize your local test suite differently from your CI, so an environment variable is provided
+to be able to easily change the number of workers a test run should use:
+
+```
+PARALLEL_WORKERS=15 bin/rails test
+```
+
+When parallelizing tests, Active Record automatically handles creating and migrating a database for each
+process. The databases will be suffixed with the number corresponding to the worker. For example, if you
+have 2 workers the tests will create `test-database-0` and `test-database-1` respectively.
+
+If the number of workers passed is 1 or fewer the processes will not be forked and the tests will not
+be parallelized and the tests will use the original `test-database` database.
+
+Two hooks are provided, one runs when the process is forked, and one runs before the processes are closed.
+These can be useful if your app uses multiple databases or perform other tasks that depend on the number of
+workers.
+
+The `parallelize_setup` method is called right after the processes are forked. The `parallelize_teardown` metod
+is called right before the processes are closed.
+
+```
+class ActiveSupport::TestCase
+ parallelize_setup do |worker|
+ # setup databases
+ end
+
+ parallelize_teardown do |worker|
+ # cleanup database
+ end
+
+ parallelize(workers: 2)
+end
+```
+
+These methods are not needed or available when using parallel testing with threads.
+
+### Parallel testing with threads
+
+If you prefer using threads or are using JRuby, a threaded parallelization option is provided. The threaded
+parallelizer is backed by Minitest's `Parallel::Executor`.
+
+To change the parallelization method to use threads over forks put the following in your `test_helper.rb`
+
+```
+class ActiveSupport::TestCase
+ parallelize(workers: 2, with: :threads)
+end
+```
+
+Rails applications generated from JRuby will automatically include the `with: :threads` option.
+
+The number of workers passed to `parallelize` determines the number of threads the tests will use. You may
+want to parallelize your local test suite differently from your CI, so an environment variable is provided
+to be able to easily change the number of workers a test run should use:
+
+```
+PARALLEL_WORKERS=15 bin/rails test
+```
+
The Test Database
-----------------
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 51b284ff12..ff7ccfd47d 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -35,6 +35,7 @@ You can find a list of all released Rails versions [here](https://rubygems.org/g
Rails generally stays close to the latest released Ruby version when it's released:
+* Rails 6 requires Ruby 2.4.1 or newer.
* Rails 5 requires Ruby 2.2.2 or newer.
* Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer.
* Rails 3.2.x is the last branch to support Ruby 1.8.7.
diff --git a/rails.gemspec b/rails.gemspec
index 4b57377871..709ce642f3 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Full-stack web application framework."
s.description = "Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.required_rubygems_version = ">= 1.8.11"
s.license = "MIT"
diff --git a/railties/.gitignore b/railties/.gitignore
index 80dd262d2f..c08562e016 100644
--- a/railties/.gitignore
+++ b/railties/.gitignore
@@ -1 +1,5 @@
-log/
+/log/
+/test/500.html
+/test/fixtures/tmp/
+/test/initializer/root/log/
+/tmp/
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index b3b35307e3..51f6e1f0ac 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,8 @@
+## Rails 6.0.0.alpha (Unreleased) ##
+
+* Rails 6 requires Ruby 2.4.1 or newer.
+
+ *Jeremy Daer*
Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md) for previous changes.
diff --git a/railties/lib/minitest/rails_plugin.rb b/railties/lib/minitest/rails_plugin.rb
index 6901b0bbc8..7193abbc33 100644
--- a/railties/lib/minitest/rails_plugin.rb
+++ b/railties/lib/minitest/rails_plugin.rb
@@ -43,10 +43,15 @@ module Minitest
Minitest.backtrace_filter = ::Rails.backtrace_cleaner if ::Rails.respond_to?(:backtrace_cleaner)
end
+ # Suppress summary reports when outputting inline rerun snippets.
+ if reporter.reporters.reject! { |reporter| reporter.kind_of?(SummaryReporter) }
+ reporter << SuppressedSummaryReporter.new(options[:io], options)
+ end
+
# Replace progress reporter for colors.
- reporter.reporters.delete_if { |reporter| reporter.kind_of?(SummaryReporter) || reporter.kind_of?(ProgressReporter) }
- reporter << SuppressedSummaryReporter.new(options[:io], options)
- reporter << ::Rails::TestUnitReporter.new(options[:io], options)
+ if reporter.reporters.reject! { |reporter| reporter.kind_of?(ProgressReporter) }
+ reporter << ::Rails::TestUnitReporter.new(options[:io], options)
+ end
end
# Backwardscompatibility with Rails 5.0 generated plugin test scripts
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index a200a1005c..a9dee10981 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -268,7 +268,8 @@ module Rails
"action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest,
"action_dispatch.cookies_rotations" => config.action_dispatch.cookies_rotations,
"action_dispatch.content_security_policy" => config.content_security_policy,
- "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only
+ "action_dispatch.content_security_policy_report_only" => config.content_security_policy_report_only,
+ "action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator
)
end
end
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 6743ab2a54..912faed3e4 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -17,47 +17,49 @@ module Rails
:session_options, :time_zone, :reload_classes_only_on_change,
:beginning_of_week, :filter_redirect, :x, :enable_dependency_loading,
:read_encrypted_secrets, :log_level, :content_security_policy_report_only,
- :require_master_key
+ :content_security_policy_nonce_generator, :require_master_key
- attr_reader :encoding, :api_only
+ attr_reader :encoding, :api_only, :loaded_config_version
def initialize(*)
super
- self.encoding = Encoding::UTF_8
- @allow_concurrency = nil
- @consider_all_requests_local = false
- @filter_parameters = []
- @filter_redirect = []
- @helpers_paths = []
- @public_file_server = ActiveSupport::OrderedOptions.new
- @public_file_server.enabled = true
- @public_file_server.index_name = "index"
- @force_ssl = false
- @ssl_options = {}
- @session_store = nil
- @time_zone = "UTC"
- @beginning_of_week = :monday
- @log_level = :debug
- @generators = app_generators
- @cache_store = [ :file_store, "#{root}/tmp/cache/" ]
- @railties_order = [:all]
- @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
- @reload_classes_only_on_change = true
- @file_watcher = ActiveSupport::FileUpdateChecker
- @exceptions_app = nil
- @autoflush_log = true
- @log_formatter = ActiveSupport::Logger::SimpleFormatter.new
- @eager_load = nil
- @secret_token = nil
- @secret_key_base = nil
- @api_only = false
- @debug_exception_response_format = nil
- @x = Custom.new
- @enable_dependency_loading = false
- @read_encrypted_secrets = false
- @content_security_policy = nil
- @content_security_policy_report_only = false
- @require_master_key = false
+ self.encoding = Encoding::UTF_8
+ @allow_concurrency = nil
+ @consider_all_requests_local = false
+ @filter_parameters = []
+ @filter_redirect = []
+ @helpers_paths = []
+ @public_file_server = ActiveSupport::OrderedOptions.new
+ @public_file_server.enabled = true
+ @public_file_server.index_name = "index"
+ @force_ssl = false
+ @ssl_options = {}
+ @session_store = nil
+ @time_zone = "UTC"
+ @beginning_of_week = :monday
+ @log_level = :debug
+ @generators = app_generators
+ @cache_store = [ :file_store, "#{root}/tmp/cache/" ]
+ @railties_order = [:all]
+ @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"]
+ @reload_classes_only_on_change = true
+ @file_watcher = ActiveSupport::FileUpdateChecker
+ @exceptions_app = nil
+ @autoflush_log = true
+ @log_formatter = ActiveSupport::Logger::SimpleFormatter.new
+ @eager_load = nil
+ @secret_token = nil
+ @secret_key_base = nil
+ @api_only = false
+ @debug_exception_response_format = nil
+ @x = Custom.new
+ @enable_dependency_loading = false
+ @read_encrypted_secrets = false
+ @content_security_policy = nil
+ @content_security_policy_report_only = false
+ @content_security_policy_nonce_generator = nil
+ @require_master_key = false
+ @loaded_config_version = nil
end
def load_defaults(target_version)
@@ -115,9 +117,14 @@ module Rails
when "6.0"
load_defaults "5.2"
+ if respond_to?(:action_view)
+ action_view.default_enforce_utf8 = false
+ end
else
raise "Unknown version #{target_version.to_s.inspect}"
end
+
+ @loaded_config_version = target_version
end
def encoding=(value)
@@ -238,7 +245,11 @@ module Rails
end
def content_security_policy(&block)
- @content_security_policy ||= ActionDispatch::ContentSecurityPolicy.new(&block)
+ if block_given?
+ @content_security_policy = ActionDispatch::ContentSecurityPolicy.new(&block)
+ else
+ @content_security_policy
+ end
end
class Custom #:nodoc:
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 73c7defe7f..433a7ab41f 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -70,7 +70,8 @@ module Rails
middleware.use ::Rack::Head
middleware.use ::Rack::ConditionalGet
middleware.use ::Rack::ETag, "no-cache"
- middleware.use ::Rack::TempfileReaper
+
+ middleware.use ::Rack::TempfileReaper unless config.api_only
end
end
diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb
index 812e846837..078e9f937f 100644
--- a/railties/lib/rails/command.rb
+++ b/railties/lib/rails/command.rb
@@ -4,7 +4,6 @@ require "active_support"
require "active_support/dependencies/autoload"
require "active_support/core_ext/enumerable"
require "active_support/core_ext/object/blank"
-require "active_support/core_ext/hash/transform_values"
require "thor"
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
index 385d3976da..fa54c0362a 100644
--- a/railties/lib/rails/commands/credentials/credentials_command.rb
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -20,7 +20,7 @@ module Rails
require_application_and_environment!
ensure_editor_available(command: "bin/rails credentials:edit") || (return)
- ensure_master_key_has_been_added
+ ensure_master_key_has_been_added if Rails.application.credentials.key.nil?
ensure_credentials_have_been_added
catch_editing_exceptions do
diff --git a/railties/lib/rails/commands/encrypted/encrypted_command.rb b/railties/lib/rails/commands/encrypted/encrypted_command.rb
index 912c453f09..3bc8f76ce4 100644
--- a/railties/lib/rails/commands/encrypted/encrypted_command.rb
+++ b/railties/lib/rails/commands/encrypted/encrypted_command.rb
@@ -21,9 +21,10 @@ module Rails
def edit(file_path)
require_application_and_environment!
+ encrypted = Rails.application.encrypted(file_path, key_path: options[:key])
ensure_editor_available(command: "bin/rails encrypted:edit") || (return)
- ensure_encryption_key_has_been_added(options[:key])
+ ensure_encryption_key_has_been_added(options[:key]) if encrypted.key.nil?
ensure_encrypted_file_has_been_added(file_path, options[:key])
catch_editing_exceptions do
diff --git a/railties/lib/rails/commands/routes/routes_command.rb b/railties/lib/rails/commands/routes/routes_command.rb
new file mode 100644
index 0000000000..c4fd6c7eb5
--- /dev/null
+++ b/railties/lib/rails/commands/routes/routes_command.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require "rails/command"
+
+module Rails
+ module Command
+ class RoutesCommand < Base # :nodoc:
+ class_option :controller, aliases: "-c", type: :string, desc: "Specifies the controller."
+ class_option :grep_pattern, aliases: "-g", type: :string, desc: "Specifies grep pattern."
+
+ no_commands do
+ def help
+ say "Usage: Print out all defined routes in match order, with names."
+ say ""
+ say "Target specific controller with -c option, or grep routes using -g option"
+ say ""
+ end
+ end
+
+ def perform(*)
+ require_application_and_environment!
+ require "action_dispatch/routing/inspector"
+
+ all_routes = Rails.application.routes.routes
+ inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
+
+ say inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, routes_filter)
+ end
+
+ private
+
+ def routes_filter
+ if options.has_key?("controller")
+ { controller: options["controller"] }
+ elsif options.has_key?("grep_pattern")
+ options["grep_pattern"]
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 3362bf629a..d85bbfb03e 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/string/strip"
+
module Rails
module Generators
module Actions
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index 863a914912..e1889979d7 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -2,7 +2,6 @@
require "fileutils"
require "digest/md5"
-require "active_support/core_ext/string/strip"
require "rails/version" unless defined?(Rails::VERSION)
require "open-uri"
require "uri"
diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb
index 98fcc95964..d6732f8ff1 100644
--- a/railties/lib/rails/generators/named_base.rb
+++ b/railties/lib/rails/generators/named_base.rb
@@ -31,12 +31,8 @@ module Rails
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :file_name
-
private
+ attr_reader :file_name
# FIXME: We are avoiding to use alias because a bug on thor that make
# this method public and add it to the task list.
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index fd9da7803f..395ac7ef2f 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -130,6 +130,8 @@ module Rails
assets_config_exist = File.exist?("config/initializers/assets.rb")
csp_config_exist = File.exist?("config/initializers/content_security_policy.rb")
+ @config_target_version = Rails.application.config.loaded_config_version || "5.0"
+
config
unless cookie_serializer_config_exist
@@ -167,7 +169,7 @@ module Rails
return if options[:pretend] || options[:dummy_app]
require "rails/generators/rails/master_key/master_key_generator"
- master_key_generator = Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet])
+ master_key_generator = Rails::Generators::MasterKeyGenerator.new([], quiet: options[:quiet], force: options[:force])
master_key_generator.add_master_key_file_silently
master_key_generator.ignore_master_key_file_silently
end
@@ -233,6 +235,10 @@ module Rails
def vendor
empty_directory_with_keep_file "vendor"
end
+
+ def config_target_version
+ defined?(@config_target_version) ? @config_target_version : Rails::VERSION::STRING.to_f
+ end
end
module Generators
@@ -242,7 +248,7 @@ module Rails
RESERVED_NAMES = %w[application destroy plugin runner test]
class AppGenerator < AppBase # :nodoc:
- WEBPACKS = %w( react vue angular elm )
+ WEBPACKS = %w( react vue angular elm stimulus )
add_shared_options_for "application"
@@ -383,9 +389,13 @@ module Rails
end
end
- def delete_application_layout_file_if_api_option
+ def delete_app_views_if_api_option
if options[:api]
- remove_file "app/views/layouts/application.html.erb"
+ if options[:skip_action_mailer]
+ remove_dir "app/views"
+ else
+ remove_file "app/views/layouts/application.html.erb"
+ end
end
end
@@ -449,7 +459,7 @@ module Rails
def delete_new_framework_defaults
unless options[:update]
- remove_file "config/initializers/new_framework_defaults_5_2.rb"
+ remove_file "config/initializers/new_framework_defaults_6_0.rb"
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
index 5460155b3e..ef715f1368 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
@@ -3,6 +3,7 @@
<head>
<title><%= camelized %></title>
<%%= csrf_meta_tags %>
+ <%%= csp_meta_tag %>
<%- if options[:skip_javascript] -%>
<%%= stylesheet_link_tag 'application', media: 'all' %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt
index d1a09f9c3c..9a427113c7 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb.tt
@@ -24,7 +24,7 @@ Bundler.require(*Rails.groups)
module <%= app_const_base %>
class Application < Rails::Application
# Initialize configuration defaults for originally generated Rails version.
- config.load_defaults <%= Rails::VERSION::STRING.to_f %>
+ config.load_defaults <%= build(:config_target_version) %>
# Settings in config/environments/* take precedence over those specified here.
# Application configuration can go into files in config/initializers
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
index 4c0f36db98..926326b5bb 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
@@ -34,8 +34,6 @@ Rails.application.configure do
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
- # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
-
<%- end -%>
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# config.action_controller.asset_host = 'http://assets.example.com'
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt
index edde7f42b8..d3bcaa5ec8 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt
@@ -10,12 +10,15 @@
# policy.img_src :self, :https, :data
# policy.object_src :none
# policy.script_src :self, :https
-# policy.style_src :self, :https, :unsafe_inline
+# policy.style_src :self, :https
# # Specify URI for violation reports
# # policy.report_uri "/csp-violation-report-endpoint"
# end
+# If you are using UJS then enable automatic nonce generation
+# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
+
# Report CSP violations to a specified URI
# For further information see the following documentation:
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt
deleted file mode 100644
index b4ef455802..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_5_2.rb.tt
+++ /dev/null
@@ -1,30 +0,0 @@
-# Be sure to restart your server when you modify this file.
-#
-# This file contains migration options to ease your Rails 5.2 upgrade.
-#
-# Once upgraded flip defaults one by one to migrate to the new default.
-#
-# Read the Guide for Upgrading Ruby on Rails for more info on each option.
-
-# Make Active Record use stable #cache_key alongside new #cache_version method.
-# This is needed for recyclable cache keys.
-# Rails.application.config.active_record.cache_versioning = true
-
-# Use AES-256-GCM authenticated encryption for encrypted cookies.
-# Existing cookies will be converted on read then written with the new scheme.
-# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true
-
-# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages
-# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true.
-# Rails.application.config.active_support.use_authenticated_message_encryption = true
-
-# Add default protection from forgery to ActionController::Base instead of in
-# ApplicationController.
-# Rails.application.config.action_controller.default_protect_from_forgery = true
-
-# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and
-# 'f' after migrating old data.
-# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
-
-# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header.
-# Rails.application.config.active_support.use_sha1_digests = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
new file mode 100644
index 0000000000..179b97de4a
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt
@@ -0,0 +1,10 @@
+# Be sure to restart your server when you modify this file.
+#
+# This file contains migration options to ease your Rails 6.0 upgrade.
+#
+# Once upgraded flip defaults one by one to migrate to the new default.
+#
+# Read the Guide for Upgrading Ruby on Rails for more info on each option.
+
+# Don't force requests from old versions of IE to be UTF-8 encoded
+# Rails.application.config.action_view.default_enforce_utf8 = false
diff --git a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
index 52d68cc77c..c918b57eca 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb.tt
@@ -3,6 +3,13 @@ require_relative '../config/environment'
require 'rails/test_help'
class ActiveSupport::TestCase
+ # Run tests in parallel with specified workers
+<% if defined?(JRUBY_VERSION) -%>
+ parallelize(workers: 2, with: :threads)
+<%- else -%>
+ parallelize(workers: 2)
+<% end -%>
+
<% unless options[:skip_active_record] -%>
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index 6d45d6e8f8..6e2495d45f 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -16,6 +16,7 @@ module Rails
def add_routes
return if options[:skip_routes]
+ return if actions.empty?
route generate_routing_code
end
diff --git a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
index 915115b63a..719e0c1e4c 100644
--- a/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
+++ b/railties/lib/rails/generators/rails/credentials/credentials_generator.rb
@@ -42,9 +42,14 @@ module Rails
end
def credentials_template
- "# aws:\n# access_key_id: 123\n# secret_access_key: 345\n\n" +
- "# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.\n" +
- "secret_key_base: #{SecureRandom.hex(64)}"
+ <<~YAML
+ # aws:
+ # access_key_id: 123
+ # secret_access_key: 345
+
+ # Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
+ secret_key_base: #{SecureRandom.hex(64)}
+ YAML
end
end
end
diff --git a/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
index 85b3663fba..867e28c6db 100644
--- a/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
+++ b/railties/lib/rails/generators/rails/encrypted_file/encrypted_file_generator.rb
@@ -15,7 +15,12 @@ module Rails
private
def encrypted_file_template
- "# aws:\n# access_key_id: 123\n# secret_access_key: 345\n\n"
+ <<~YAML
+ # aws:
+ # access_key_id: 123
+ # secret_access_key: 345
+
+ YAML
end
end
end
diff --git a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
index 82968985dc..21664ea86d 100644
--- a/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
+++ b/railties/lib/rails/generators/rails/master_key/master_key_generator.rb
@@ -27,7 +27,9 @@ module Rails
end
def add_master_key_file_silently(key = nil)
- key_file_generator.add_key_file_silently(MASTER_KEY_PATH, key)
+ unless MASTER_KEY_PATH.exist?
+ key_file_generator.add_key_file_silently(MASTER_KEY_PATH, key)
+ end
end
def ignore_master_key_file
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
index abbacd9bec..b86ef0f2f8 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationController < ActionController::#{api? ? "API" : "Base"}
#{ api? ? '# ' : '' }protect_from_forgery with: :exception
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
index 25d692732d..be078f36de 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
module ApplicationHelper
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
index bad1ff2d16..846863bc13 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationJob < ActiveJob::Base
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
index 09aac13f42..246e274348 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
index 8aa3de78f1..21465278be 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
index 6bc480161d..6e54a1ce9d 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt
@@ -2,9 +2,13 @@
<html>
<head>
<title><%= humanized %></title>
+ <%%= csrf_meta_tags %>
+ <%%= csp_meta_tag %>
+
<%%= stylesheet_link_tag "<%= namespaced_name %>/application", media: "all" %>
+ <%- unless options[:skip_javascript] -%>
<%%= javascript_include_tag "<%= namespaced_name %>/application" %>
- <%%= csrf_meta_tags %>
+ <%- end -%>
</head>
<body>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
index b3264509fc..ee8e469da2 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
@@ -19,10 +19,10 @@ require "rails"
require "active_model/railtie"
require "active_job/railtie"
<%= comment_if :skip_active_record %>require "active_record/railtie"
+<%= comment_if :skip_active_storage %>require "active_storage/engine"
require "action_controller/railtie"
<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
require "action_view/railtie"
-require "active_storage/engine"
<%= comment_if :skip_action_cable %>require "action_cable/engine"
<%= comment_if :skip_sprockets %>require "sprockets/railtie"
<%= comment_if :skip_test %>require "rails/test_unit/railtie"
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt
index 8938770fc4..4ec1804940 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class Engine < ::Rails::Engine
#{mountable? ? ' isolate_namespace ' + camelized_modules : ' '}
#{api? ? " config.generators.api_only = true" : ' '}
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt
index 7bdf4ee5fb..b853fabcc3 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt
@@ -1,4 +1,4 @@
-<%= wrap_in_modules <<-rb.strip_heredoc
+<%= wrap_in_modules <<~rb
class Railtie < ::Rails::Railtie
end
rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt
index f3d80c87f5..51049826bf 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js.tt
@@ -10,6 +10,7 @@
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
+//= require rails-ujs
<% unless skip_active_storage? -%>
//= require activestorage
<% end -%>
diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb
index a146a8fda6..5675faff70 100644
--- a/railties/lib/rails/generators/resource_helpers.rb
+++ b/railties/lib/rails/generators/resource_helpers.rb
@@ -25,13 +25,8 @@ module Rails
assign_controller_names!(controller_name.pluralize)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :controller_name, :controller_file_name
-
private
+ attr_reader :controller_name, :controller_file_name
def controller_class_path
if options[:model_name]
diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb
index 76b6b80d28..f8d3311156 100644
--- a/railties/lib/rails/ruby_version_check.rb
+++ b/railties/lib/rails/ruby_version_check.rb
@@ -1,15 +1,15 @@
# frozen_string_literal: true
-if RUBY_VERSION < "2.2.2" && RUBY_ENGINE == "ruby"
+if RUBY_VERSION < "2.4.1" && RUBY_ENGINE == "ruby"
desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
abort <<-end_message
- Rails 5 requires Ruby 2.2.2 or newer.
+ Rails 6 requires Ruby 2.4.1 or newer.
You're running
#{desc}
- Please upgrade to Ruby 2.2.2 or newer to continue.
+ Please upgrade to Ruby 2.4.1 or newer to continue.
end_message
end
diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb
index 30e3478c9b..747cf31d7a 100644
--- a/railties/lib/rails/secrets.rb
+++ b/railties/lib/rails/secrets.rb
@@ -2,7 +2,6 @@
require "yaml"
require "active_support/message_encryptor"
-require "active_support/core_ext/string/strip"
module Rails
# Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘
diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb
index 2f644a20c9..56f2eba312 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -12,7 +12,6 @@ require "rake"
middleware
misc
restart
- routes
tmp
yarn
).tap { |arr|
diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake
deleted file mode 100644
index 403286d280..0000000000
--- a/railties/lib/rails/tasks/routes.rake
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require "optparse"
-
-desc "Print out all defined routes in match order, with names. Target specific controller with -c option, or grep routes using -g option"
-task routes: :environment do
- all_routes = Rails.application.routes.routes
- require "action_dispatch/routing/inspector"
- inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
-
- routes_filter = nil
-
- OptionParser.new do |opts|
- opts.banner = "Usage: rails routes [options]"
-
- Rake.application.standard_rake_options.each { |args| opts.on(*args) }
-
- opts.on("-c CONTROLLER") do |controller|
- routes_filter = { controller: controller }
- end
-
- opts.on("-g PATTERN") do |pattern|
- routes_filter = pattern
- end
-
- end.parse!(ARGV.reject { |x| x == "routes" })
-
- puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, routes_filter)
-
- exit 0 # ensure extra arguments aren't interpreted as Rake tasks
-end
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index 76c28ac85e..4bd7d74b04 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -22,6 +22,7 @@ if defined?(ActiveRecord::Base)
module ActiveSupport
class TestCase
+ include ActiveRecord::TestDatabases
include ActiveRecord::TestFixtures
self.fixture_path = "#{Rails.root}/test/fixtures/"
self.file_fixture_path = fixture_path + "files"
diff --git a/railties/railties.gemspec b/railties/railties.gemspec
index 4c665cd546..1df8b1fe39 100644
--- a/railties/railties.gemspec
+++ b/railties/railties.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Tools for creating, working with, and running Rails applications."
s.description = "Rails internals: application bootup, plugins, generators, and rake tasks."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index 236d73d5fd..bd9b87467c 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1766,7 +1766,7 @@ module ApplicationTests
test "represent_boolean_as_integer should be able to set via config.active_record.sqlite3.represent_boolean_as_integer" do
remove_from_config '.*config\.load_defaults.*\n'
- app_file "config/initializers/new_framework_defaults_5_2.rb", <<-RUBY
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
RUBY
@@ -1905,8 +1905,8 @@ module ApplicationTests
test "ActiveSupport::MessageEncryptor.use_authenticated_message_encryption can be configured via config.active_support.use_authenticated_message_encryption" do
remove_from_config '.*config\.load_defaults.*\n'
- app_file "config/initializers/new_framework_defaults_5_2.rb", <<-RUBY
- Rails.application.config.active_support.use_authenticated_message_encryption = true
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.active_support.use_authenticated_message_encryption = true
RUBY
app "development"
@@ -1931,8 +1931,8 @@ module ApplicationTests
test "ActiveSupport::Digest.hash_digest_class can be configured via config.active_support.use_sha1_digests" do
remove_from_config '.*config\.load_defaults.*\n'
- app_file "config/initializers/new_framework_defaults_5_2.rb", <<-RUBY
- Rails.application.config.active_support.use_sha1_digests = true
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.active_support.use_sha1_digests = true
RUBY
app "development"
@@ -1940,6 +1940,44 @@ module ApplicationTests
assert_equal Digest::SHA1, ActiveSupport::Digest.hash_digest_class
end
+ test "custom serializers should be able to set via config.active_job.custom_serializers in an initializer" do
+ class ::DummySerializer < ActiveJob::Serializers::ObjectSerializer; end
+
+ app_file "config/initializers/custom_serializers.rb", <<-RUBY
+ Rails.application.config.active_job.custom_serializers << DummySerializer
+ RUBY
+
+ app "development"
+
+ assert_includes ActiveJob::Serializers.serializers, DummySerializer
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is false by default" do
+ app "development"
+ assert_equal false, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is true in an upgraded app" do
+ remove_from_config '.*config\.load_defaults.*\n'
+ add_to_config 'config.load_defaults "5.2"'
+
+ app "development"
+
+ assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
+ test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 can be configured via config.action_view.default_enforce_utf8" do
+ remove_from_config '.*config\.load_defaults.*\n'
+
+ app_file "config/initializers/new_framework_defaults_6_0.rb", <<-RUBY
+ Rails.application.config.action_view.default_enforce_utf8 = true
+ RUBY
+
+ app "development"
+
+ assert_equal true, ActionView::Helpers::FormTagHelper.default_enforce_utf8
+ end
+
private
def force_lazy_load_hooks
yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it.
diff --git a/railties/test/application/content_security_policy_test.rb b/railties/test/application/content_security_policy_test.rb
index 97f2957c33..0d28df16f8 100644
--- a/railties/test/application/content_security_policy_test.rb
+++ b/railties/test/application/content_security_policy_test.rb
@@ -16,7 +16,7 @@ module ApplicationTests
teardown_app
end
- test "default content security policy is empty" do
+ test "default content security policy is nil" do
controller :pages, <<-RUBY
class PagesController < ApplicationController
def index
@@ -34,7 +34,33 @@ module ApplicationTests
app("development")
get "/"
- assert_equal ";", last_response.headers["Content-Security-Policy"]
+ assert_nil last_response.headers["Content-Security-Policy"]
+ end
+
+ test "empty content security policy is generated" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/content_security_policy.rb", <<-RUBY
+ Rails.application.config.content_security_policy do |p|
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy ""
end
test "global content security policy in an initializer" do
@@ -61,7 +87,7 @@ module ApplicationTests
app("development")
get "/"
- assert_policy "default-src 'self' https:;"
+ assert_policy "default-src 'self' https:"
end
test "global report only content security policy in an initializer" do
@@ -90,7 +116,7 @@ module ApplicationTests
app("development")
get "/"
- assert_policy "default-src 'self' https:;", report_only: true
+ assert_policy "default-src 'self' https:", report_only: true
end
test "override content security policy in a controller" do
@@ -121,7 +147,7 @@ module ApplicationTests
app("development")
get "/"
- assert_policy "default-src https://example.com;"
+ assert_policy "default-src https://example.com"
end
test "override content security policy to report only in a controller" do
@@ -150,7 +176,7 @@ module ApplicationTests
app("development")
get "/"
- assert_policy "default-src 'self' https:;", report_only: true
+ assert_policy "default-src 'self' https:", report_only: true
end
test "global content security policy added to rack app" do
@@ -174,7 +200,7 @@ module ApplicationTests
app("development")
get "/"
- assert_policy "default-src 'self' https:;"
+ assert_policy "default-src 'self' https:"
end
private
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index 2632dd7cde..889ad16fb8 100644
--- a/railties/test/application/loading_test.rb
+++ b/railties/test/application/loading_test.rb
@@ -359,6 +359,18 @@ class LoadingTest < ActiveSupport::TestCase
assert_predicate Rails.application, :initialized?
end
+ test "frameworks aren't loaded during initialization" do
+ app_file "config/initializers/raise_when_frameworks_load.rb", <<-RUBY
+ %i(action_controller action_mailer active_job active_record).each do |framework|
+ ActiveSupport.on_load(framework) { raise "\#{framework} loaded!" }
+ end
+ RUBY
+
+ assert_nothing_raised do
+ require "#{app_path}/config/environment"
+ end
+ end
+
private
def setup_ar!
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index b9e4a9ccc0..5efaf841d4 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -70,8 +70,7 @@ module ApplicationTests
"ActionDispatch::Callbacks",
"Rack::Head",
"Rack::ConditionalGet",
- "Rack::ETag",
- "Rack::TempfileReaper"
+ "Rack::ETag"
], middleware
end
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index 5a6404bd0a..f7d301e125 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -2,7 +2,6 @@
require "isolation/abstract_unit"
require "env_helpers"
-require "active_support/core_ext/string/strip"
module ApplicationTests
class RakeTest < ActiveSupport::TestCase
@@ -131,7 +130,7 @@ module ApplicationTests
RUBY
output = rails("routes")
- assert_equal <<-MESSAGE.strip_heredoc, output
+ assert_equal <<~MESSAGE, output
Prefix Verb URI Pattern Controller#Action
cart GET /cart(.:format) cart#show
rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
@@ -163,117 +162,6 @@ module ApplicationTests
assert_equal expected_output, output
end
- def test_rails_routes_with_global_search_key
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- post '/cart', to: 'cart#create'
- get '/basketballs', to: 'basketball#index'
- end
- RUBY
-
- output = rails("routes", "-g", "show")
- assert_equal <<-MESSAGE.strip_heredoc, output
- Prefix Verb URI Pattern Controller#Action
- cart GET /cart(.:format) cart#show
- rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
- rails_blob_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show
- rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#show
- rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
- MESSAGE
-
- output = rails("routes", "-g", "POST")
- assert_equal <<-MESSAGE.strip_heredoc, output
- Prefix Verb URI Pattern Controller#Action
- POST /cart(.:format) cart#create
- rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
- MESSAGE
-
- output = rails("routes", "-g", "basketballs")
- assert_equal " Prefix Verb URI Pattern Controller#Action\n" \
- "basketballs GET /basketballs(.:format) basketball#index\n", output
- end
-
- def test_rails_routes_with_controller_search_key
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- get '/basketball', to: 'basketball#index'
- end
- RUBY
-
- output = rails("routes", "-c", "cart")
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
-
- output = rails("routes", "-c", "Cart")
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
-
- output = rails("routes", "-c", "CartController")
- assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
- end
-
- def test_rails_routes_with_namespaced_controller_search_key
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- namespace :admin do
- resource :post
- end
- end
- RUBY
- expected_output = [" Prefix Verb URI Pattern Controller#Action",
- " new_admin_post GET /admin/post/new(.:format) admin/posts#new",
- "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit",
- " admin_post GET /admin/post(.:format) admin/posts#show",
- " PATCH /admin/post(.:format) admin/posts#update",
- " PUT /admin/post(.:format) admin/posts#update",
- " DELETE /admin/post(.:format) admin/posts#destroy",
- " POST /admin/post(.:format) admin/posts#create\n"].join("\n")
-
- output = rails("routes", "-c", "Admin::PostController")
- assert_equal expected_output, output
-
- output = rails("routes", "-c", "PostController")
- assert_equal expected_output, output
- end
-
- def test_rails_routes_displays_message_when_no_routes_are_defined
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- end
- RUBY
-
- assert_equal <<-MESSAGE.strip_heredoc, rails("routes")
- Prefix Verb URI Pattern Controller#Action
- rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
- rails_blob_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show
- rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#show
- rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
- update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
- rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
- MESSAGE
- end
-
- def test_rake_routes_with_rake_options
- app_file "config/routes.rb", <<-RUBY
- Rails.application.routes.draw do
- get '/cart', to: 'cart#show'
- end
- RUBY
-
- output = Dir.chdir(app_path) { `bin/rake --rakefile Rakefile routes` }
-
- assert_equal <<-MESSAGE.strip_heredoc, output
- Prefix Verb URI Pattern Controller#Action
- cart GET /cart(.:format) cart#show
- rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
- rails_blob_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show
- rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#show
- rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
- update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
- rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
- MESSAGE
- end
-
def test_logger_is_flushed_when_exiting_production_rake_tasks
add_to_config <<-RUBY
rake_tasks do
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index a01325fdb8..8e5ccf94cc 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "isolation/abstract_unit"
-require "active_support/core_ext/string/strip"
require "env_helpers"
module ApplicationTests
@@ -502,10 +501,10 @@ module ApplicationTests
end
def test_output_inline_by_default
- create_test_file :models, "post", pass: false
+ create_test_file :models, "post", pass: false, print: false
output = run_test_command("test/models/post_test.rb")
- expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nbin/rails test test/models/post_test.rb:4\n\n\n\n}
+ expect = %r{Running:\n\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nbin/rails test test/models/post_test.rb:4\n\n\n\n}
assert_match expect, output
end
@@ -523,6 +522,28 @@ module ApplicationTests
capture(:stderr) { run_test_command("test/models/post_test.rb --fail-fast", stderr: true) })
end
+ def test_run_in_parallel_with_processes
+ file_name = create_parallel_processes_test_file
+
+ output = run_test_command(file_name)
+
+ assert_match %r{Finished in.*\n2 runs, 2 assertions}, output
+ end
+
+ def test_run_in_parallel_with_threads
+ app_path("/test/test_helper.rb") do |file_name|
+ file = File.read(file_name)
+ file.sub!(/parallelize\(([^\)]*)\)/, "parallelize(\\1, with: :threads)")
+ File.write(file_name, file)
+ end
+
+ file_name = create_parallel_threads_test_file
+
+ output = run_test_command(file_name)
+
+ assert_match %r{Finished in.*\n2 runs, 2 assertions}, output
+ end
+
def test_raise_error_when_specified_file_does_not_exist
error = capture(:stderr) { run_test_command("test/not_exists.rb", stderr: true) }
assert_match(%r{cannot load such file.+test/not_exists\.rb}, error)
@@ -718,7 +739,7 @@ module ApplicationTests
def create_model_with_fixture
rails "generate", "model", "user", "name:string"
- app_file "test/fixtures/users.yml", <<-YAML.strip_heredoc
+ app_file "test/fixtures/users.yml", <<~YAML
vampire:
id: 1
name: Koyomi Araragi
@@ -800,19 +821,70 @@ module ApplicationTests
RUBY
end
- def create_test_file(path = :unit, name = "test", pass: true)
+ def create_test_file(path = :unit, name = "test", pass: true, print: true)
app_file "test/#{path}/#{name}_test.rb", <<-RUBY
require 'test_helper'
class #{name.camelize}Test < ActiveSupport::TestCase
def test_truth
- puts "#{name.camelize}Test"
+ puts "#{name.camelize}Test" if #{print}
assert #{pass}, 'wups!'
end
end
RUBY
end
+ def create_parallel_processes_test_file
+ app_file "test/models/parallel_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class ParallelTest < ActiveSupport::TestCase
+ RD1, WR1 = IO.pipe
+ RD2, WR2 = IO.pipe
+
+ test "one" do
+ WR1.close
+ assert_equal "x", RD1.read(1) # blocks until two runs
+
+ RD2.close
+ WR2.write "y" # Allow two to run
+ WR2.close
+ end
+
+ test "two" do
+ RD1.close
+ WR1.write "x" # Allow one to run
+ WR1.close
+
+ WR2.close
+ assert_equal "y", RD2.read(1) # blocks until one runs
+ end
+ end
+ RUBY
+ end
+
+ def create_parallel_threads_test_file
+ app_file "test/models/parallel_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class ParallelTest < ActiveSupport::TestCase
+ Q1 = Queue.new
+ Q2 = Queue.new
+ test "one" do
+ assert_equal "x", Q1.pop # blocks until two runs
+
+ Q2 << "y"
+ end
+
+ test "two" do
+ Q1 << "x"
+
+ assert_equal "y", Q2.pop # blocks until one runs
+ end
+ end
+ RUBY
+ end
+
def create_env_test
app_file "test/unit/env_test.rb", <<-RUBY
require 'test_helper'
diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb
index 0a51e98656..fb43bebfbe 100644
--- a/railties/test/application/test_test.rb
+++ b/railties/test/application/test_test.rb
@@ -7,10 +7,15 @@ module ApplicationTests
include ActiveSupport::Testing::Isolation
def setup
+ @old = ENV["PARALLEL_WORKERS"]
+ ENV["PARALLEL_WORKERS"] = "0"
+
build_app
end
def teardown
+ ENV["PARALLEL_WORKERS"] = @old
+
teardown_app
end
diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb
index 7c464b3fde..663ee73bcd 100644
--- a/railties/test/commands/credentials_test.rb
+++ b/railties/test/commands/credentials_test.rb
@@ -43,6 +43,18 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
assert_match(/api_key: abc/, run_show_command)
end
+ test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do
+ Dir.chdir(app_path) do
+ key = IO.binread("config/master.key").strip
+ FileUtils.rm("config/master.key")
+
+ switch_env("RAILS_MASTER_KEY", key) do
+ run_edit_command
+ assert_not File.exist?("config/master.key")
+ end
+ end
+ end
+
test "show credentials" do
assert_match(/access_key_id: 123/, run_show_command)
end
diff --git a/railties/test/commands/encrypted_test.rb b/railties/test/commands/encrypted_test.rb
index 6647dcc902..9fc73d5f18 100644
--- a/railties/test/commands/encrypted_test.rb
+++ b/railties/test/commands/encrypted_test.rb
@@ -33,6 +33,18 @@ class Rails::Command::EncryptedCommandTest < ActiveSupport::TestCase
end
end
+ test "edit command does not add master key when `RAILS_MASTER_KEY` env specified" do
+ Dir.chdir(app_path) do
+ key = IO.binread("config/master.key").strip
+ FileUtils.rm("config/master.key")
+
+ switch_env("RAILS_MASTER_KEY", key) do
+ run_edit_command("config/tokens.yml.enc")
+ assert_not File.exist?("config/master.key")
+ end
+ end
+ end
+
test "edit encrypts file with custom key" do
run_edit_command("config/tokens.yml.enc", key: "config/tokens.key")
diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb
new file mode 100644
index 0000000000..030dcc7217
--- /dev/null
+++ b/railties/test/commands/routes_test.rb
@@ -0,0 +1,127 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "env_helpers"
+require "rails/command"
+require "rails/commands/routes/routes_command"
+
+class Rails::Command::RoutesTest < ActiveSupport::TestCase
+ setup :build_app
+ teardown :teardown_app
+
+ test "test singular resource output in rake routes" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ resource :post
+ end
+ RUBY
+
+ expected_output = [" Prefix Verb URI Pattern Controller#Action",
+ " new_post GET /post/new(.:format) posts#new",
+ "edit_post GET /post/edit(.:format) posts#edit",
+ " post GET /post(.:format) posts#show",
+ " PATCH /post(.:format) posts#update",
+ " PUT /post(.:format) posts#update",
+ " DELETE /post(.:format) posts#destroy",
+ " POST /post(.:format) posts#create\n"].join("\n")
+
+ output = run_routes_command(["-c", "PostController"])
+ assert_equal expected_output, output
+ end
+
+ test "test rails routes with global search key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ post '/cart', to: 'cart#create'
+ get '/basketballs', to: 'basketball#index'
+ end
+ RUBY
+
+ output = run_routes_command(["-g", "show"])
+ assert_equal <<~MESSAGE, output
+ Prefix Verb URI Pattern Controller#Action
+ cart GET /cart(.:format) cart#show
+ rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
+ rails_blob_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show
+ rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#show
+ rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
+ MESSAGE
+
+ output = run_routes_command(["-g", "POST"])
+ assert_equal <<~MESSAGE, output
+ Prefix Verb URI Pattern Controller#Action
+ POST /cart(.:format) cart#create
+ rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+
+ output = run_routes_command(["-g", "basketballs"])
+ assert_equal " Prefix Verb URI Pattern Controller#Action\n" \
+ "basketballs GET /basketballs(.:format) basketball#index\n", output
+ end
+
+ test "test rails routes with controller search_key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ get '/basketball', to: 'basketball#index'
+ end
+ RUBY
+
+ output = run_routes_command(["-c", "cart"])
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
+
+ output = run_routes_command(["routes", "-c", "Cart"])
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
+
+ output = run_routes_command(["-c", "CartController"])
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
+ end
+
+ test "test rails routes with namespaced controller search key" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ namespace :admin do
+ resource :post
+ end
+ end
+ RUBY
+ expected_output = [" Prefix Verb URI Pattern Controller#Action",
+ " new_admin_post GET /admin/post/new(.:format) admin/posts#new",
+ "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit",
+ " admin_post GET /admin/post(.:format) admin/posts#show",
+ " PATCH /admin/post(.:format) admin/posts#update",
+ " PUT /admin/post(.:format) admin/posts#update",
+ " DELETE /admin/post(.:format) admin/posts#destroy",
+ " POST /admin/post(.:format) admin/posts#create\n"].join("\n")
+
+ output = run_routes_command(["-c", "Admin::PostController"])
+ assert_equal expected_output, output
+
+ output = run_routes_command(["-c", "PostController"])
+ assert_equal expected_output, output
+ end
+
+ test "test rails routes displays message when no routes are defined" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ end
+ RUBY
+
+ assert_equal <<~MESSAGE, run_routes_command
+ Prefix Verb URI Pattern Controller#Action
+ rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show
+ rails_blob_variation GET /rails/active_storage/variants/:signed_blob_id/:variation_key/*filename(.:format) active_storage/variants#show
+ rails_blob_preview GET /rails/active_storage/previews/:signed_blob_id/:variation_key/*filename(.:format) active_storage/previews#show
+ rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show
+ update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update
+ rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create
+ MESSAGE
+ end
+
+ private
+
+ def run_routes_command(args = [])
+ rails "routes", args
+ end
+end
diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb
index 4815cf6362..9c523ad372 100644
--- a/railties/test/generators/api_app_generator_test.rb
+++ b/railties/test/generators/api_app_generator_test.rb
@@ -13,7 +13,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
Rails.application = TestApp::Application
super
- Kernel::silence_warnings do
+ Kernel.silence_warnings do
Thor::Base.shell.send(:attr_accessor, :always_force)
@shell = Thor::Base.shell.new
@shell.send(:always_force=, true)
@@ -63,6 +63,23 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_generator_if_skip_action_mailer_is_given
+ run_generator [destination_root, "--api", "--skip-action-mailer"]
+ assert_file "config/application.rb", /#\s+require\s+["']action_mailer\/railtie["']/
+ assert_file "config/environments/development.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "config/environments/test.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_file "config/environments/production.rb" do |content|
+ assert_no_match(/config\.action_mailer/, content)
+ end
+ assert_no_directory "app/mailers"
+ assert_no_directory "test/mailers"
+ assert_no_directory "app/views"
+ end
+
def test_app_update_does_not_generate_unnecessary_config_files
run_generator
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 60b9ff1317..1d2e0fd354 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -211,7 +211,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
def test_new_application_doesnt_need_defaults
- assert_no_file "config/initializers/new_framework_defaults_5_2.rb"
+ assert_no_file "config/initializers/new_framework_defaults_6_0.rb"
end
def test_new_application_load_defaults
@@ -219,6 +219,8 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [app_root]
output = nil
+ assert_file "#{app_root}/config/application.rb", /\s+config\.load_defaults #{Rails::VERSION::STRING.to_f}/
+
Dir.chdir(app_root) do
output = `./bin/rails r "puts Rails.application.config.assets.unknown_asset_fallback"`
end
@@ -257,14 +259,14 @@ class AppGeneratorTest < Rails::Generators::TestCase
app_root = File.join(destination_root, "myapp")
run_generator [app_root]
- assert_no_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb"
+ assert_no_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb"
stub_rails_application(app_root) do
generator = Rails::Generators::AppGenerator.new ["rails"], { update: true }, { destination_root: app_root, shell: @shell }
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
- assert_file "#{app_root}/config/initializers/new_framework_defaults_5_2.rb"
+ assert_file "#{app_root}/config/initializers/new_framework_defaults_6_0.rb"
end
end
@@ -313,6 +315,15 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile", /^# gem 'mini_magick'/
end
+ def test_mini_magick_gem_when_skip_active_storage_is_given
+ app_root = File.join(destination_root, "myapp")
+ run_generator [app_root, "--skip-active-storage"]
+
+ assert_file "#{app_root}/Gemfile" do |content|
+ assert_no_match(/gem 'mini_magick'/, content)
+ end
+ end
+
def test_app_update_does_not_generate_active_storage_contents_when_skip_active_storage_is_given
app_root = File.join(destination_root, "myapp")
run_generator [app_root, "--skip-active-storage"]
@@ -334,10 +345,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
assert_no_file "#{app_root}/config/storage.yml"
-
- assert_file "#{app_root}/Gemfile" do |content|
- assert_no_match(/gem 'mini_magick'/, content)
- end
end
def test_app_update_does_not_generate_active_storage_contents_when_skip_active_record_is_given
@@ -361,10 +368,19 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
assert_no_file "#{app_root}/config/storage.yml"
+ end
- assert_file "#{app_root}/Gemfile" do |content|
- assert_no_match(/gem 'mini_magick'/, content)
+ def test_app_update_does_not_change_config_target_version
+ run_generator
+
+ FileUtils.cd(destination_root) do
+ config = "config/application.rb"
+ content = File.read(config)
+ File.write(config, content.gsub(/config\.load_defaults #{Rails::VERSION::STRING.to_f}/, "config.load_defaults 5.1"))
+ quietly { system("bin/rails app:update") }
end
+
+ assert_file "config/application.rb", /\s+config\.load_defaults 5\.1/
end
def test_application_names_are_not_singularized
@@ -638,6 +654,13 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_empty output
end
+ def test_force_option_overwrites_every_file_except_master_key
+ run_generator [File.join(destination_root, "myapp")]
+ output = run_generator [File.join(destination_root, "myapp"), "--force"]
+ assert_match(/force/, output)
+ assert_no_match("force config/master.key", output)
+ end
+
def test_application_name_with_spaces
path = File.join(destination_root, "foo bar")
diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb
index a3218951a6..91e4a86775 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -109,4 +109,11 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_match(/^ namespace :admin do\n get 'dashboard\/index'\n get 'dashboard\/show'\n end$/, route)
end
end
+
+ def test_does_not_add_routes_when_action_is_not_specified
+ run_generator ["admin/dashboard"]
+ assert_file "config/routes.rb" do |routes|
+ assert_no_match(/namespace :admin/, routes)
+ end
+ end
end
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index 516aa0704f..8d933e82c3 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -2,7 +2,6 @@
require "generators/generators_test_helper"
require "rails/generators/rails/model/model_generator"
-require "active_support/core_ext/string/strip"
class ModelGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -379,10 +378,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_belongs_to_adds_required_association
run_generator ["account", "supplier:references{required}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ApplicationRecord
- belongs_to :supplier, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -390,10 +389,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_polymorphic_belongs_to_generages_correct_model
run_generator ["account", "supplier:references{required,polymorphic}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ApplicationRecord
- belongs_to :supplier, polymorphic: true, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, polymorphic: true, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -401,10 +400,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_required_and_polymorphic_are_order_independent
run_generator ["account", "supplier:references{polymorphic.required}"]
- expected_file = <<-FILE.strip_heredoc
- class Account < ApplicationRecord
- belongs_to :supplier, polymorphic: true, required: true
- end
+ expected_file = <<~FILE
+ class Account < ApplicationRecord
+ belongs_to :supplier, polymorphic: true, required: true
+ end
FILE
assert_file "app/models/account.rb", expected_file
end
@@ -452,11 +451,11 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_token_option_adds_has_secure_token
run_generator ["user", "token:token", "auth_token:token"]
- expected_file = <<-FILE.strip_heredoc
- class User < ApplicationRecord
- has_secure_token
- has_secure_token :auth_token
- end
+ expected_file = <<~FILE
+ class User < ApplicationRecord
+ has_secure_token
+ has_secure_token :auth_token
+ end
FILE
assert_file "app/models/user.rb", expected_file
end
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index fc7584c175..28ac3611b7 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -82,11 +82,12 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
def test_generating_in_full_mode_with_almost_of_all_skip_options
- run_generator [destination_root, "--full", "-M", "-O", "-C", "-S", "-T"]
+ run_generator [destination_root, "--full", "-M", "-O", "-C", "-S", "-T", "--skip-active-storage"]
assert_file "bin/rails" do |content|
assert_no_match(/\s+require\s+["']rails\/all["']/, content)
end
assert_file "bin/rails", /#\s+require\s+["']active_record\/railtie["']/
+ assert_file "bin/rails", /#\s+require\s+["']active_storage\/engine["']/
assert_file "bin/rails", /#\s+require\s+["']action_mailer\/railtie["']/
assert_file "bin/rails", /#\s+require\s+["']action_cable\/engine["']/
assert_file "bin/rails", /#\s+require\s+["']sprockets\/railtie["']/
@@ -216,12 +217,22 @@ class PluginGeneratorTest < Rails::Generators::TestCase
def test_javascripts_generation
run_generator [destination_root, "--mountable"]
- assert_file "app/assets/javascripts/bukkits/application.js"
+ assert_file "app/assets/javascripts/bukkits/application.js" do |content|
+ assert_match "//= require rails-ujs", content
+ assert_match "//= require activestorage", content
+ assert_match "//= require_tree .", content
+ end
+ assert_file "app/views/layouts/bukkits/application.html.erb" do |content|
+ assert_match "javascript_include_tag", content
+ end
end
def test_skip_javascripts
run_generator [destination_root, "--skip-javascript", "--mountable"]
assert_no_file "app/assets/javascripts/bukkits/application.js"
+ assert_file "app/views/layouts/bukkits/application.html.erb" do |content|
+ assert_no_match "javascript_include_tag", content
+ end
end
def test_template_from_dir_pwd
@@ -320,8 +331,11 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/
assert_file "app/views/layouts/bukkits/application.html.erb" do |contents|
assert_match "<title>Bukkits</title>", contents
+ assert_match "<%= csrf_meta_tags %>", contents
+ assert_match "<%= csp_meta_tag %>", contents
assert_match(/stylesheet_link_tag\s+['"]bukkits\/application['"]/, contents)
assert_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents)
+ assert_match "<%= yield %>", contents
end
assert_file "test/test_helper.rb" do |content|
assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content)
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 97d43af60a..aa577e4234 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -9,7 +9,7 @@ module SharedGeneratorTests
super
Rails::Generators::AppGenerator.instance_variable_set("@desc", nil)
- Kernel::silence_warnings do
+ Kernel.silence_warnings do
Thor::Base.shell.send(:attr_accessor, :always_force)
@shell = Thor::Base.shell.new
@shell.send(:always_force=, true)
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 6568a356d6..0a4d2a9167 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -38,7 +38,12 @@ module TestHelpers
end
def app_path(*args)
- tmp_path(*%w[app] + args)
+ path = tmp_path(*%w[app] + args)
+ if block_given?
+ yield path
+ else
+ path
+ end
end
def framework_path
diff --git a/railties/test/minitest/rails_plugin_test.rb b/railties/test/minitest/rails_plugin_test.rb
new file mode 100644
index 0000000000..7c3a2022a9
--- /dev/null
+++ b/railties/test/minitest/rails_plugin_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class Minitest::RailsPluginTest < ActiveSupport::TestCase
+ setup do
+ @options = Minitest.process_args []
+ @output = StringIO.new("".encode("UTF-8"))
+ end
+
+ test "default reporters are replaced" do
+ with_reporter Minitest::CompositeReporter.new do |reporter|
+ reporter << Minitest::SummaryReporter.new(@output, @options)
+ reporter << Minitest::ProgressReporter.new(@output, @options)
+ reporter << Minitest::Reporter.new(@output, @options)
+
+ Minitest.plugin_rails_init({})
+
+ assert_equal 3, reporter.reporters.count
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(Minitest::SuppressedSummaryReporter) }
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(::Rails::TestUnitReporter) }
+ assert reporter.reporters.any? { |candidate| candidate.kind_of?(Minitest::Reporter) }
+ end
+ end
+
+ test "no custom reporters are added if nothing to replace" do
+ with_reporter Minitest::CompositeReporter.new do |reporter|
+ Minitest.plugin_rails_init({})
+
+ assert_empty reporter.reporters
+ end
+ end
+
+ private
+ def with_reporter(reporter)
+ old_reporter, Minitest.reporter = Minitest.reporter, reporter
+
+ yield reporter
+ ensure
+ Minitest.reporter = old_reporter
+ end
+end