aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock16
-rw-r--r--README.md4
-rw-r--r--actioncable/test/client_test.rb2
-rw-r--r--actionmailbox/CHANGELOG.md4
-rw-r--r--actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb2
-rw-r--r--actionmailbox/lib/action_mailbox/router.rb2
-rw-r--r--actionmailbox/lib/action_mailbox/test_helper.rb6
-rw-r--r--actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb24
-rw-r--r--actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb2
-rw-r--r--actionmailbox/test/unit/router_test.rb9
-rw-r--r--actionmailer/lib/action_mailer/base.rb4
-rw-r--r--actionmailer/test/base_test.rb11
-rw-r--r--actionpack/CHANGELOG.md78
-rw-r--r--actionpack/lib/abstract_controller.rb1
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb2
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb4
-rw-r--r--actionpack/lib/action_controller.rb2
-rw-r--r--actionpack/lib/action_controller/base.rb1
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb3
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb5
-rw-r--r--actionpack/lib/action_controller/metal/content_security_policy.rb2
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb2
-rw-r--r--actionpack/lib/action_controller/metal/feature_policy.rb46
-rw-r--r--actionpack/lib/action_controller/metal/head.rb2
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb2
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb14
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb2
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb2
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb6
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb4
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb2
-rw-r--r--actionpack/lib/action_controller/metal/rescue.rb2
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb19
-rw-r--r--actionpack/lib/action_controller/renderer.rb7
-rw-r--r--actionpack/lib/action_controller/test_case.rb4
-rw-r--r--actionpack/lib/action_dispatch.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/content_security_policy.rb35
-rw-r--r--actionpack/lib/action_dispatch/http/feature_policy.rb168
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb20
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb10
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb7
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb44
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb27
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb6
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb18
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb2
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb36
-rw-r--r--actionpack/lib/action_dispatch/system_testing/browser.rb23
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb2
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb52
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb11
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb12
-rw-r--r--actionpack/test/controller/integration_test.rb41
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb2
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb8
-rw-r--r--actionpack/test/controller/parameters/mutators_test.rb18
-rw-r--r--actionpack/test/controller/render_js_test.rb9
-rw-r--r--actionpack/test/controller/render_json_test.rb18
-rw-r--r--actionpack/test/controller/render_test.rb24
-rw-r--r--actionpack/test/controller/render_xml_test.rb9
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb9
-rw-r--r--actionpack/test/controller/resources_test.rb3
-rw-r--r--actionpack/test/dispatch/content_security_policy_test.rb78
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb31
-rw-r--r--actionpack/test/dispatch/exception_wrapper_test.rb2
-rw-r--r--actionpack/test/dispatch/feature_policy_test.rb142
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb6
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb2
-rw-r--r--actionpack/test/dispatch/request/json_params_parsing_test.rb2
-rw-r--r--actionpack/test/dispatch/request_test.rb18
-rw-r--r--actionpack/test/dispatch/routing_test.rb10
-rw-r--r--actionpack/test/dispatch/ssl_test.rb4
-rw-r--r--actionpack/test/dispatch/system_testing/driver_test.rb13
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb79
-rw-r--r--actionpack/test/dispatch/system_testing/system_test_case_test.rb8
-rw-r--r--actiontext/app/helpers/action_text/content_helper.rb2
-rw-r--r--actiontext/app/helpers/action_text/tag_helper.rb1
-rw-r--r--actiontext/lib/action_text/attachables/remote_image.rb2
-rw-r--r--actiontext/lib/action_text/attachment.rb2
-rw-r--r--actiontext/lib/action_text/attachment_gallery.rb2
-rw-r--r--actiontext/lib/action_text/attachments/trix_conversion.rb2
-rw-r--r--actiontext/lib/action_text/engine.rb4
-rw-r--r--actiontext/package.json2
-rw-r--r--actiontext/test/dummy/config/initializers/backtrace_silencers.rb2
-rw-r--r--actiontext/test/unit/attachment_test.rb5
-rw-r--r--actionview/CHANGELOG.md13
-rw-r--r--actionview/app/assets/javascripts/rails-ujs/utils/form.coffee1
-rw-r--r--actionview/lib/action_view/base.rb1
-rw-r--r--actionview/lib/action_view/digestor.rb9
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb3
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb9
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb35
-rw-r--r--actionview/lib/action_view/helpers/tags/date_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_local_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/month_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/time_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/tags/week_field.rb2
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb109
-rw-r--r--actionview/lib/action_view/layouts.rb4
-rw-r--r--actionview/lib/action_view/lookup_context.rb3
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb2
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb4
-rw-r--r--actionview/lib/action_view/template.rb2
-rw-r--r--actionview/lib/action_view/template/error.rb20
-rw-r--r--actionview/lib/action_view/template/handlers.rb2
-rw-r--r--actionview/lib/action_view/template/html.rb2
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb38
-rw-r--r--actionview/lib/action_view/unbound_template.rb6
-rw-r--r--actionview/test/activerecord/relation_cache_test.rb17
-rw-r--r--actionview/test/template/date_helper_i18n_test.rb1
-rw-r--r--actionview/test/template/date_helper_test.rb1
-rw-r--r--actionview/test/template/form_options_helper_test.rb2
-rw-r--r--actionview/test/template/output_safety_helper_test.rb8
-rw-r--r--actionview/test/template/template_error_test.rb16
-rw-r--r--actionview/test/template/testing/fixture_resolver_test.rb22
-rw-r--r--actionview/test/template/url_helper_test.rb128
-rw-r--r--actionview/test/ujs/public/test/data-remote.js18
-rw-r--r--activejob/CHANGELOG.md3
-rw-r--r--activejob/lib/active_job/core.rb4
-rw-r--r--activejob/lib/active_job/test_helper.rb3
-rw-r--r--activejob/test/cases/test_helper_test.rb18
-rw-r--r--activejob/test/jobs/timezone_dependent_job.rb1
-rw-r--r--activejob/test/support/integration/dummy_app_template.rb2
-rw-r--r--activemodel/CHANGELOG.md6
-rw-r--r--activemodel/lib/active_model.rb1
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb10
-rw-r--r--activemodel/lib/active_model/dirty.rb10
-rw-r--r--activemodel/lib/active_model/error.rb95
-rw-r--r--activemodel/lib/active_model/errors.rb99
-rw-r--r--activemodel/lib/active_model/railtie.rb2
-rw-r--r--activemodel/lib/active_model/secure_password.rb20
-rw-r--r--activemodel/lib/active_model/type/float.rb2
-rw-r--r--activemodel/lib/active_model/type/helpers/timezone.rb2
-rw-r--r--activemodel/lib/active_model/validations/format.rb2
-rw-r--r--activemodel/lib/active_model/validations/validates.rb4
-rw-r--r--activemodel/test/cases/errors_test.rb68
-rw-r--r--activemodel/test/cases/helper.rb1
-rw-r--r--activemodel/test/cases/railtie_test.rb6
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb99
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb1
-rw-r--r--activerecord/CHANGELOG.md39
-rw-r--r--activerecord/lib/active_record/associations/association.rb4
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb19
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/autosave_association.rb4
-rw-r--r--activerecord/lib/active_record/base.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb6
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb10
-rw-r--r--activerecord/lib/active_record/core.rb2
-rw-r--r--activerecord/lib/active_record/database_configurations.rb91
-rw-r--r--activerecord/lib/active_record/enum.rb9
-rw-r--r--activerecord/lib/active_record/errors.rb20
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb6
-rw-r--r--activerecord/lib/active_record/fixtures.rb62
-rw-r--r--activerecord/lib/active_record/insert_all.rb6
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver.rb12
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb1
-rw-r--r--activerecord/lib/active_record/persistence.rb10
-rw-r--r--activerecord/lib/active_record/query_cache.rb4
-rw-r--r--activerecord/lib/active_record/railtie.rb1
-rw-r--r--activerecord/lib/active_record/railties/databases.rake33
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb2
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb60
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb6
-rw-r--r--activerecord/lib/active_record/serialization.rb2
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb16
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb8
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb6
-rw-r--r--activerecord/lib/arel/nodes/node.rb8
-rw-r--r--activerecord/lib/arel/predications.rb4
-rw-r--r--activerecord/lib/arel/visitors.rb1
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb203
-rw-r--r--activerecord/lib/arel/visitors/mysql.rb8
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb4
-rw-r--r--activerecord/lib/arel/visitors/visitor.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/annotate_test.rb37
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb27
-rw-r--r--activerecord/test/cases/adapters/mysql2/set_test.rb32
-rw-r--r--activerecord/test/cases/adapters/mysql2/transaction_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/annotate_test.rb37
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb18
-rw-r--r--activerecord/test/cases/adapters/sqlite3/annotate_test.rb37
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb16
-rw-r--r--activerecord/test/cases/annotate_test.rb46
-rw-r--r--activerecord/test/cases/arel/attributes/attribute_test.rb24
-rw-r--r--activerecord/test/cases/arel/nodes/node_test.rb21
-rw-r--r--activerecord/test/cases/arel/select_manager_test.rb12
-rw-r--r--activerecord/test/cases/arel/visitors/depth_first_test.rb276
-rw-r--r--activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb8
-rw-r--r--activerecord/test/cases/arel/visitors/mysql_test.rb46
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb7
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb11
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb25
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb47
-rw-r--r--activerecord/test/cases/autosave_association_test.rb57
-rw-r--r--activerecord/test/cases/batches_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb91
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb2
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb134
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb2
-rw-r--r--activerecord/test/cases/database_configurations_test.rb13
-rw-r--r--activerecord/test/cases/database_selector_test.rb34
-rw-r--r--activerecord/test/cases/dirty_test.rb6
-rw-r--r--activerecord/test/cases/enum_test.rb24
-rw-r--r--activerecord/test/cases/finder_test.rb8
-rw-r--r--activerecord/test/cases/fixtures_test.rb29
-rw-r--r--activerecord/test/cases/inheritance_test.rb4
-rw-r--r--activerecord/test/cases/insert_all_test.rb18
-rw-r--r--activerecord/test/cases/query_cache_test.rb17
-rw-r--r--activerecord/test/cases/relation_test.rb7
-rw-r--r--activerecord/test/cases/relations_test.rb6
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb7
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb2
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb4
-rw-r--r--activerecord/test/fixtures/other_books.yml26
-rw-r--r--activerecord/test/fixtures/parrots.yml8
-rw-r--r--activerecord/test/models/author.rb1
-rw-r--r--activerecord/test/models/book.rb6
-rw-r--r--activerecord/test/models/drink_designer.rb6
-rw-r--r--activerecord/test/models/mouse.rb6
-rw-r--r--activerecord/test/models/ship.rb3
-rw-r--r--activerecord/test/models/squeak.rb6
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb4
-rw-r--r--activerecord/test/schema/schema.rb12
-rw-r--r--activestorage/CHANGELOG.md4
-rw-r--r--activestorage/app/jobs/active_storage/mirror_job.rb2
-rw-r--r--activestorage/config/routes.rb2
-rw-r--r--activestorage/lib/active_storage.rb19
-rw-r--r--activestorage/lib/active_storage/attached/model.rb19
-rw-r--r--activestorage/lib/active_storage/engine.rb3
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb6
-rw-r--r--activestorage/lib/active_storage/service/disk_service.rb6
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb16
-rw-r--r--activestorage/test/analyzer/video_analyzer_test.rb1
-rw-r--r--activestorage/test/dummy/config/initializers/backtrace_silencers.rb2
-rw-r--r--activestorage/test/fixtures/files/racecar.tifbin33705838 -> 729182 bytes
-rw-r--r--activestorage/test/models/attached/many_test.rb26
-rw-r--r--activestorage/test/service/azure_storage_service_test.rb29
-rw-r--r--activestorage/test/service/disk_service_test.rb10
-rw-r--r--activestorage/test/service/s3_service_test.rb17
-rw-r--r--activestorage/test/test_helper.rb1
-rw-r--r--activesupport/CHANGELOG.md61
-rw-r--r--activesupport/activesupport.gemspec2
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb4
-rw-r--r--activesupport/lib/active_support/cache.rb10
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb6
-rw-r--r--activesupport/lib/active_support/callbacks.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb46
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/marshal.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/name_error.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb124
-rw-r--r--activesupport/lib/active_support/core_ext/string/access.rb29
-rw-r--r--activesupport/lib/active_support/dependencies.rb40
-rw-r--r--activesupport/lib/active_support/dependencies/zeitwerk_integration.rb15
-rw-r--r--activesupport/lib/active_support/deprecation/proxy_wrappers.rb29
-rw-r--r--activesupport/lib/active_support/descendants_tracker.rb6
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb10
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb1
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb57
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb30
-rw-r--r--activesupport/lib/active_support/log_subscriber.rb3
-rw-r--r--activesupport/lib/active_support/logger.rb2
-rw-r--r--activesupport/lib/active_support/logger_thread_safe_level.rb3
-rw-r--r--activesupport/lib/active_support/message_verifier.rb4
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb2
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb2
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb3
-rw-r--r--activesupport/lib/active_support/number_helper.rb2
-rw-r--r--activesupport/lib/active_support/option_merger.rb2
-rw-r--r--activesupport/lib/active_support/parameter_filter.rb9
-rw-r--r--activesupport/lib/active_support/per_thread_registry.rb2
-rw-r--r--activesupport/lib/active_support/rails.rb3
-rw-r--r--activesupport/lib/active_support/secure_compare_rotator.rb51
-rw-r--r--activesupport/lib/active_support/tagged_logging.rb15
-rw-r--r--activesupport/lib/active_support/testing/parallelization.rb11
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb7
-rw-r--r--activesupport/test/cache/behaviors/local_cache_behavior.rb9
-rw-r--r--activesupport/test/cache/stores/mem_cache_store_test.rb2
-rw-r--r--activesupport/test/cache/stores/redis_cache_store_test.rb2
-rw-r--r--activesupport/test/callbacks_test.rb2
-rw-r--r--activesupport/test/core_ext/enumerable_test.rb24
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb1
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb41
-rw-r--r--activesupport/test/core_ext/module_test.rb29
-rw-r--r--activesupport/test/core_ext/object/acts_like_test.rb5
-rw-r--r--activesupport/test/core_ext/object/deep_dup_test.rb2
-rw-r--r--activesupport/test/core_ext/object/duplicable_test.rb2
-rw-r--r--activesupport/test/core_ext/object/instance_variables_test.rb2
-rw-r--r--activesupport/test/core_ext/object/json_gem_encoding_test.rb2
-rw-r--r--activesupport/test/core_ext/object/try_test.rb2
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb34
-rw-r--r--activesupport/test/current_attributes_test.rb10
-rw-r--r--activesupport/test/dependencies_test.rb7
-rw-r--r--activesupport/test/deprecation_test.rb17
-rw-r--r--activesupport/test/file_update_checker_shared_tests.rb394
-rw-r--r--activesupport/test/logger_test.rb44
-rw-r--r--activesupport/test/message_encryptor_test.rb4
-rw-r--r--activesupport/test/multibyte_chars_test.rb6
-rw-r--r--activesupport/test/notifications_test.rb21
-rw-r--r--activesupport/test/parameter_filter_test.rb11
-rw-r--r--activesupport/test/secure_compare_rotator_test.rb57
-rw-r--r--activesupport/test/time_zone_test.rb7
-rw-r--r--activesupport/test/transliterate_test.rb49
-rw-r--r--activesupport/test/xml_mini_test.rb1
-rw-r--r--guides/rails_guides/kindle.rb2
-rw-r--r--guides/source/5_2_release_notes.md2
-rw-r--r--guides/source/6_0_release_notes.md12
-rw-r--r--guides/source/action_controller_overview.md2
-rw-r--r--guides/source/action_mailer_basics.md2
-rw-r--r--guides/source/action_view_overview.md28
-rw-r--r--guides/source/active_record_multiple_databases.md2
-rw-r--r--guides/source/active_storage_overview.md6
-rw-r--r--guides/source/active_support_core_extensions.md13
-rw-r--r--guides/source/autoloading_and_reloading_constants.md1376
-rw-r--r--guides/source/autoloading_and_reloading_constants_classic_mode.md1351
-rw-r--r--guides/source/command_line.md4
-rw-r--r--guides/source/configuring.md13
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md2
-rw-r--r--guides/source/documents.yaml8
-rw-r--r--guides/source/engines.md4
-rw-r--r--guides/source/form_helpers.md2
-rw-r--r--guides/source/getting_started.md2
-rw-r--r--guides/source/initialization.md4
-rw-r--r--guides/source/layouts_and_rendering.md46
-rw-r--r--guides/source/rails_application_templates.md4
-rw-r--r--guides/source/routing.md20
-rw-r--r--guides/source/security.md2
-rw-r--r--guides/source/upgrading_ruby_on_rails.md110
-rw-r--r--guides/source/working_with_javascript_in_rails.md18
-rw-r--r--railties/lib/rails.rb1
-rw-r--r--railties/lib/rails/application.rb4
-rw-r--r--railties/lib/rails/application/configuration.rb18
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb1
-rw-r--r--railties/lib/rails/code_statistics.rb2
-rw-r--r--railties/lib/rails/code_statistics_calculator.rb10
-rw-r--r--railties/lib/rails/command.rb1
-rw-r--r--railties/lib/rails/command/base.rb2
-rw-r--r--railties/lib/rails/command/behavior.rb2
-rw-r--r--railties/lib/rails/command/environment_argument.rb2
-rw-r--r--railties/lib/rails/commands/credentials/USAGE15
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb27
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command/diffing.rb41
-rw-r--r--railties/lib/rails/engine.rb3
-rw-r--r--railties/lib/rails/engine/configuration.rb11
-rw-r--r--railties/lib/rails/generators.rb7
-rw-r--r--railties/lib/rails/generators/actions.rb26
-rw-r--r--railties/lib/rails/generators/app_base.rb2
-rw-r--r--railties/lib/rails/generators/base.rb9
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile.tt2
-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/environments/test.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/content_security_policy.rb.tt3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/feature_policy.rb.tt11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_6_0.rb.tt9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/robots.txt2
-rw-r--r--railties/lib/rails/paths.rb10
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb10
-rw-r--r--railties/lib/rails/tasks/framework.rake2
-rw-r--r--railties/lib/rails/tasks/zeitwerk.rake95
-rw-r--r--railties/lib/rails/test_unit/runner.rb2
-rw-r--r--railties/test/application/assets_test.rb2
-rw-r--r--railties/test/application/bin_setup_test.rb2
-rw-r--r--railties/test/application/configuration_test.rb26
-rw-r--r--railties/test/application/content_security_policy_test.rb32
-rw-r--r--railties/test/application/current_attributes_integration_test.rb4
-rw-r--r--railties/test/application/feature_policy_test.rb191
-rw-r--r--railties/test/application/generators_test.rb10
-rw-r--r--railties/test/application/middleware_test.rb1
-rw-r--r--railties/test/application/rake/dbs_test.rb44
-rw-r--r--railties/test/application/rake/multi_dbs_test.rb19
-rw-r--r--railties/test/application/rake_test.rb2
-rw-r--r--railties/test/application/system_test_case_test.rb45
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb38
-rw-r--r--railties/test/commands/credentials_test.rb70
-rw-r--r--railties/test/generators/actions_test.rb92
-rw-r--r--railties/test/generators/app_generator_test.rb6
-rw-r--r--railties/test/generators/namespaced_generators_test.rb4
-rw-r--r--railties/test/railties/engine_test.rb2
425 files changed, 6651 insertions, 3323 deletions
diff --git a/Gemfile b/Gemfile
index 9eb449316c..3a7a564a35 100644
--- a/Gemfile
+++ b/Gemfile
@@ -9,8 +9,8 @@ gemspec
# We need a newish Rake since Active Job sets its test tasks' descriptions.
gem "rake", ">= 11.1"
-gem "capybara", ">= 2.15"
-gem "selenium-webdriver", ">= 3.5.0"
+gem "capybara", ">= 3.26"
+gem "selenium-webdriver", ">= 3.141.592"
gem "rack-cache", "~> 1.2"
gem "sass-rails"
diff --git a/Gemfile.lock b/Gemfile.lock
index 85d38303f4..db32b3748e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -71,7 +71,7 @@ PATH
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
- zeitwerk (~> 2.1, >= 2.1.4)
+ zeitwerk (~> 2.1, >= 2.1.8)
rails (6.1.0.alpha)
actioncable (= 6.1.0.alpha)
actionmailbox (= 6.1.0.alpha)
@@ -175,13 +175,13 @@ GEM
bunny (2.13.0)
amq-protocol (~> 2.3, >= 2.3.0)
byebug (10.0.2)
- capybara (3.10.1)
+ capybara (3.26.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
- regexp_parser (~> 1.2)
+ regexp_parser (~> 1.5)
xpath (~> 3.2)
childprocess (1.0.1)
rake (< 13.0)
@@ -376,7 +376,7 @@ GEM
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
- rails-html-sanitizer (1.0.4)
+ rails-html-sanitizer (1.1.0)
loofah (~> 2.2, >= 2.2.2)
rainbow (3.0.0)
rake (12.3.2)
@@ -388,7 +388,7 @@ GEM
redis (4.1.1)
redis-namespace (1.6.0)
redis (>= 3.0.4)
- regexp_parser (1.3.0)
+ regexp_parser (1.6.0)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
@@ -523,7 +523,7 @@ GEM
websocket-extensions (0.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.1.4)
+ zeitwerk (2.1.8)
PLATFORMS
java
@@ -545,7 +545,7 @@ DEPENDENCIES
blade-sauce_labs_plugin
bootsnap (>= 1.4.4)
byebug
- capybara (>= 2.15)
+ capybara (>= 3.26)
connection_pool
dalli
delayed_job
@@ -582,7 +582,7 @@ DEPENDENCIES
rubocop-rails
sass-rails
sdoc (~> 1.0)
- selenium-webdriver (>= 3.5.0)
+ selenium-webdriver (>= 3.141.592)
sequel
sidekiq
sneakers
diff --git a/README.md b/README.md
index 98ade9b445..fe3b5b9d9a 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,7 @@ the [Active Model](activemodel/README.rdoc) module.
## Controller layer
The _**Controller layer**_ is responsible for handling incoming HTTP requests and
-providing a suitable response. Usually this means returning HTML, but Rails controllers
+providing a suitable response. Usually, this means returning HTML, but Rails controllers
can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and
manipulate models, and render view templates in order to generate the appropriate HTTP response.
In Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and
@@ -37,7 +37,7 @@ are bundled together in [Action Pack](actionpack/README.rdoc).
The _**View layer**_ is composed of "templates" that are responsible for providing
appropriate representations of your application's resources. Templates can
come in a variety of formats, but most view templates are HTML with embedded
-Ruby code (ERB files). Views are typically rendered to generate a controller response,
+Ruby code (ERB files). Views are typically rendered to generate a controller response
or to generate the body of an email. In Rails, View generation is handled by [Action View](actionview/README.rdoc).
## Frameworks and libraries
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
index bf141df458..eed514db10 100644
--- a/actioncable/test/client_test.rb
+++ b/actioncable/test/client_test.rb
@@ -91,7 +91,7 @@ class ClientTest < ActionCable::TestCase
rescue RuntimeError => ex
# Work around https://bugs.ruby-lang.org/issues/13239
- raise unless ex.message =~ /can't modify frozen IOError/
+ raise unless ex.message.match?(/can't modify frozen IOError/)
# Handle this as if it were the IOError: do the same as above.
server.binder.close
diff --git a/actionmailbox/CHANGELOG.md b/actionmailbox/CHANGELOG.md
index bca3d1d9ed..0d4799a7b8 100644
--- a/actionmailbox/CHANGELOG.md
+++ b/actionmailbox/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Fix Bcc header not being included with emails from `create_inbound_email_from` test helpers.
+
+ *jduff*
+
* Add `ApplicationMailbox.mailbox_for` to expose mailbox routing.
*James Dabbs*
diff --git a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb
index 8713f545f5..cd8b12dcde 100644
--- a/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb
+++ b/actionmailbox/app/controllers/rails/conductor/action_mailbox/inbound_emails_controller.rb
@@ -23,7 +23,7 @@ module Rails
Mail.new(params.require(:mail).permit(:from, :to, :cc, :bcc, :in_reply_to, :subject, :body).to_h).tap do |mail|
mail[:bcc]&.include_in_headers = true
params[:mail][:attachments].to_a.each do |attachment|
- mail.add_file(filename: attachment.path, content: attachment.read)
+ mail.add_file(filename: attachment.original_filename, content: attachment.read)
end
end
end
diff --git a/actionmailbox/lib/action_mailbox/router.rb b/actionmailbox/lib/action_mailbox/router.rb
index 54982eae21..9ac58e5b3f 100644
--- a/actionmailbox/lib/action_mailbox/router.rb
+++ b/actionmailbox/lib/action_mailbox/router.rb
@@ -31,7 +31,7 @@ module ActionMailbox
end
def mailbox_for(inbound_email)
- routes.detect { |route| route.match?(inbound_email) }.try(:mailbox_class)
+ routes.detect { |route| route.match?(inbound_email) }&.mailbox_class
end
private
diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb
index d248fa8734..dc6a0229f7 100644
--- a/actionmailbox/lib/action_mailbox/test_helper.rb
+++ b/actionmailbox/lib/action_mailbox/test_helper.rb
@@ -14,7 +14,11 @@ module ActionMailbox
#
# create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!")
def create_inbound_email_from_mail(status: :processing, **mail_options)
- create_inbound_email_from_source Mail.new(mail_options).to_s, status: status
+ mail = Mail.new(mail_options)
+ # Bcc header is not encoded by default
+ mail[:bcc].include_in_headers = true if mail[:bcc]
+
+ create_inbound_email_from_source mail.to_s, status: status
end
# Create an +InboundEmail+ using the raw rfc822 +source+ as text.
diff --git a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb
index 1b8ca7bd5e..33282d36f7 100644
--- a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb
+++ b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb
@@ -10,6 +10,7 @@ class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispa
mail: {
from: "Jason Fried <jason@37signals.com>",
to: "Replies <replies@example.com>",
+ cc: "CC <cc@example.com>",
bcc: "Bcc <bcc@example.com>",
in_reply_to: "<4e6e35f5a38b4_479f13bb90078178@small-app-01.mail>",
subject: "Hey there",
@@ -21,6 +22,7 @@ class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispa
mail = ActionMailbox::InboundEmail.last.mail
assert_equal %w[ jason@37signals.com ], mail.from
assert_equal %w[ replies@example.com ], mail.to
+ assert_equal %w[ cc@example.com ], mail.cc
assert_equal %w[ bcc@example.com ], mail.bcc
assert_equal "4e6e35f5a38b4_479f13bb90078178@small-app-01.mail", mail.in_reply_to
assert_equal "Hey there", mail.subject
@@ -28,6 +30,27 @@ class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispa
end
end
+ test "create inbound email with bcc" do
+ with_rails_env("development") do
+ assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
+ post rails_conductor_inbound_emails_path, params: {
+ mail: {
+ from: "Jason Fried <jason@37signals.com>",
+ bcc: "Replies <replies@example.com>",
+ subject: "Hey there",
+ body: "How's it going?"
+ }
+ }
+ end
+
+ mail = ActionMailbox::InboundEmail.last.mail
+ assert_equal %w[ jason@37signals.com ], mail.from
+ assert_equal %w[ replies@example.com ], mail.bcc
+ assert_equal "Hey there", mail.subject
+ assert_equal "How's it going?", mail.body.decoded
+ end
+ end
+
test "create inbound email with attachments" do
with_rails_env("development") do
assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do
@@ -45,6 +68,7 @@ class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispa
mail = ActionMailbox::InboundEmail.last.mail
assert_equal "Let's talk about these images:", mail.text_part.decoded
assert_equal 2, mail.attachments.count
+ assert_equal %w[ avatar1.jpeg avatar2.jpeg ], mail.attachments.collect(&:filename)
end
end
diff --git a/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb b/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb
index 59385cdf37..3c56b21b3c 100644
--- a/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb
+++ b/actionmailbox/test/dummy/config/initializers/backtrace_silencers.rb
@@ -1,7 +1,7 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!
diff --git a/actionmailbox/test/unit/router_test.rb b/actionmailbox/test/unit/router_test.rb
index 4dd3730604..7eb8e04a73 100644
--- a/actionmailbox/test/unit/router_test.rb
+++ b/actionmailbox/test/unit/router_test.rb
@@ -51,6 +51,15 @@ module ActionMailbox
assert_equal inbound_email.mail, $processed_mail
end
+ test "single string routing on bcc" do
+ @router.add_routes("first@example.com" => :first)
+
+ inbound_email = create_inbound_email_from_mail(to: "someone@example.com", bcc: "first@example.com", subject: "This is a reply")
+ @router.route inbound_email
+ assert_equal "FirstMailbox", $processed_by
+ assert_equal inbound_email.mail, $processed_mail
+ end
+
test "single string routing case-insensitively" do
@router.add_routes("first@example.com" => :first)
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index ba914c4813..78264d21d1 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -408,7 +408,7 @@ module ActionMailer
# really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
# of an OpenSSL verify constant (<tt>'none'</tt> or <tt>'peer'</tt>) or directly the constant
# (<tt>OpenSSL::SSL::VERIFY_NONE</tt> or <tt>OpenSSL::SSL::VERIFY_PEER</tt>).
- # <tt>:ssl/:tls</tt> Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection)
+ # * <tt>:ssl/:tls</tt> Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection)
#
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
@@ -737,7 +737,7 @@ module ActionMailer
end
class LateAttachmentsProxy < SimpleDelegator
- def inline; _raise_error end
+ def inline; self end
def []=(_name, _content); _raise_error end
private
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 4ea5634cd0..6fa5be54cb 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -267,6 +267,17 @@ class BaseTest < ActiveSupport::TestCase
assert_match(/Can't add attachments after `mail` was called./, e.message)
end
+ test "accessing inline attachments after mail was called works" do
+ class LateInlineAttachmentAccessorMailer < ActionMailer::Base
+ def welcome
+ mail body: "yay", from: "welcome@example.com", to: "to@example.com"
+ attachments.inline["invoice.pdf"]
+ end
+ end
+
+ assert_nothing_raised { LateInlineAttachmentAccessorMailer.welcome.message }
+ end
+
test "adding inline attachments while rendering mail works" do
class LateInlineAttachmentMailer < ActionMailer::Base
def on_render
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 55592585ea..90217755bc 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,79 @@
+* Allow system test screen shots to be taken more than once in
+ a test by prefixing the file name with an incrementing counter.
+
+ Add an environment variable `RAILS_SYSTEM_TESTING_SCREENSHOT_HTML` to
+ enable saving of HTML during a screenshot in addition to the image.
+ This uses the same image name, with the extension replaced with `.html`
+
+ *Tom Fakes*
+
+* Add `Vary: Accept` header when using `Accept` header for response
+
+ For some requests like `/users/1`, Rails uses requests' `Accept`
+ header to determine what to return. And if we don't add `Vary`
+ in the response header, browsers might accidentally cache different
+ types of content, which would cause issues: e.g. javascript got displayed
+ instead of html content. This PR fixes these issues by adding `Vary: Accept`
+ in these types of requests. For more detailed problem description, please read:
+
+ https://github.com/rails/rails/pull/36213
+
+ Fixes #25842
+
+ *Stan Lo*
+
+* Fix IntegrationTest `follow_redirect!` to follow redirection using the same HTTP verb when following
+ a 307 redirection.
+
+ *Edouard Chin*
+
+* System tests require Capybara 3.26 or newer.
+
+ *George Claghorn*
+
+* Reduced log noise handling ActionController::RoutingErrors.
+
+ *Alberto Fernández-Capel*
+
+* Add DSL for configuring HTTP Feature Policy
+
+ This new DSL provides a way to configure a HTTP Feature Policy at a
+ global or per-controller level. Full details of HTTP Feature Policy
+ specification and guidelines can be found at MDN:
+
+ https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
+
+ Example global policy
+
+ ```
+ Rails.application.config.feature_policy do |f|
+ f.camera :none
+ f.gyroscope :none
+ f.microphone :none
+ f.usb :none
+ f.fullscreen :self
+ f.payment :self, "https://secure.example.com"
+ end
+ ```
+
+ Example controller level policy
+
+ ```
+ class PagesController < ApplicationController
+ feature_policy do |p|
+ p.geolocation "https://example.com"
+ end
+ end
+ ```
+
+ *Jacob Bednarz*
+
+* Add the ability to set the CSP nonce only to the specified directives.
+
+ Fixes #35137.
+
+ *Yuji Yaginuma*
+
* Keep part when scope option has value.
When a route was defined within an optional scope, if that route didn't
@@ -13,7 +89,7 @@
*Gustavo Gutierrez*
-* Calling `ActionController::Parameters#transform_keys/!` without a block now returns
+* Calling `ActionController::Parameters#transform_keys`/`!` without a block now returns
an enumerator for the parameters instead of the underlying hash.
*Eugene Kenny*
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index 3a98931167..d1ff62a032 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "action_pack"
+require "active_support"
require "active_support/rails"
require "active_support/i18n"
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index 42bab411d2..983d65f944 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -37,7 +37,7 @@ module AbstractController
# Override <tt>AbstractController::Base#process_action</tt> to run the
# <tt>process_action</tt> callbacks around the normal behavior.
- def process_action(*args)
+ def process_action(*)
run_callbacks(:process_action) do
super
end
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 8ba2b25552..280af50a44 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -28,6 +28,7 @@ module AbstractController
else
_set_rendered_content_type rendered_format
end
+ _set_vary_header
self.response_body = rendered_body
end
@@ -109,6 +110,9 @@ module AbstractController
def _set_html_content_type # :nodoc:
end
+ def _set_vary_header # :nodoc:
+ end
+
def _set_rendered_content_type(format) # :nodoc:
end
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 29d61c3ceb..22dc229599 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require "active_support/rails"
require "abstract_controller"
require "action_dispatch"
require "action_controller/metal/live"
@@ -28,6 +27,7 @@ module ActionController
autoload :DefaultHeaders
autoload :EtagWithTemplateDigest
autoload :EtagWithFlash
+ autoload :FeaturePolicy
autoload :Flash
autoload :ForceSSL
autoload :Head
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 2e565d5d44..63c138af55 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -226,6 +226,7 @@ module ActionController
FormBuilder,
RequestForgeryProtection,
ContentSecurityPolicy,
+ FeaturePolicy,
ForceSSL,
Streaming,
DataStreaming,
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index d8b04d8ddb..033ad10d4c 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -22,8 +22,7 @@ module ActionController
additions = ActionController::Base.log_process_action(payload)
status = payload[:status]
- if status.nil? && payload[:exception].present?
- exception_class_name = payload[:exception].first
+ if status.nil? && (exception_class_name = payload[:exception].first)
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
end
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index 29d1919ec5..967bda38f2 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -1,5 +1,8 @@
# frozen_string_literal: true
+require "active_support/core_ext/object/try"
+require "active_support/core_ext/integer/time"
+
module ActionController
module ConditionalGet
extend ActiveSupport::Concern
@@ -17,7 +20,7 @@ module ActionController
# of cached pages.
#
# class InvoicesController < ApplicationController
- # etag { current_user.try :id }
+ # etag { current_user&.id }
#
# def show
# # Etag will differ even for the same invoice when it's viewed by a different current_user
diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb
index ebd90f07c8..25fc110bfe 100644
--- a/actionpack/lib/action_controller/metal/content_security_policy.rb
+++ b/actionpack/lib/action_controller/metal/content_security_policy.rb
@@ -45,7 +45,7 @@ module ActionController #:nodoc:
end
def current_content_security_policy
- request.content_security_policy.try(:clone) || ActionDispatch::ContentSecurityPolicy.new
+ request.content_security_policy&.clone || ActionDispatch::ContentSecurityPolicy.new
end
end
end
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 9ef4f50df1..879745a895 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -53,7 +53,7 @@ module ActionController #:nodoc:
#
# Show a 404 page in the browser:
#
- # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', status: 404
+ # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', disposition: 'inline', status: 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description) in
diff --git a/actionpack/lib/action_controller/metal/feature_policy.rb b/actionpack/lib/action_controller/metal/feature_policy.rb
new file mode 100644
index 0000000000..a627eabea6
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/feature_policy.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module ActionController #:nodoc:
+ # HTTP Feature Policy is a web standard for defining a mechanism to
+ # allow and deny the use of browser features in its own context, and
+ # in content within any <iframe> elements in the document.
+ #
+ # Full details of HTTP Feature Policy specification and guidelines can
+ # be found at MDN:
+ #
+ # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
+ #
+ # Examples of usage:
+ #
+ # # Global policy
+ # Rails.application.config.feature_policy do |f|
+ # f.camera :none
+ # f.gyroscope :none
+ # f.microphone :none
+ # f.usb :none
+ # f.fullscreen :self
+ # f.payment :self, "https://secure.example.com"
+ # end
+ #
+ # # Controller level policy
+ # class PagesController < ApplicationController
+ # feature_policy do |p|
+ # p.geolocation "https://example.com"
+ # end
+ # end
+ module FeaturePolicy
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def feature_policy(**options, &block)
+ before_action(options) do
+ if block_given?
+ policy = request.feature_policy.clone
+ yield policy
+ request.feature_policy = policy
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
index 3c84bebb85..f290272055 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -29,7 +29,7 @@ module ActionController
content_type = options.delete(:content_type)
options.each do |key, value|
- headers[key.to_s.dasherize.split("-").each { |v| v[0] = v[0].chr.upcase }.join("-")] = value.to_s
+ headers[key.to_s.split(/[-_]/).each { |v| v[0] = v[0].upcase }.join("-")] = value.to_s
end
self.status = status
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 6a274d35cb..ec0c9ecc67 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -482,7 +482,7 @@ module ActionController
def raw_params(auth)
_raw_params = auth.sub(TOKEN_REGEX, "").split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
- if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}})
+ if !(%r{\A#{TOKEN_KEY}}.match?(_raw_params.first))
_raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
end
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index 6f7fc0d624..594c4c1dc8 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -16,7 +16,7 @@ module ActionController
attr_internal :view_runtime
- def process_action(*args)
+ def process_action(*)
raw_payload = {
controller: self.class.name,
action: action_name,
@@ -27,18 +27,18 @@ module ActionController
path: request.fullpath
}
- ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload.dup)
+ ActiveSupport::Notifications.instrument("start_processing.action_controller", raw_payload)
ActiveSupport::Notifications.instrument("process_action.action_controller", raw_payload) do |payload|
- super.tap do
- payload[:status] = response.status
- end
+ result = super
+ payload[:status] = response.status
+ result
ensure
append_info_to_payload(payload)
end
end
- def render(*args)
+ def render(*)
render_output = nil
self.view_runtime = cleanup_view_runtime do
Benchmark.ms { render_output = super }
@@ -59,7 +59,7 @@ module ActionController
end
end
- def redirect_to(*args)
+ def redirect_to(*)
ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload|
result = super
payload[:status] = response.status
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 5c6f7fe396..a993c76af9 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -142,7 +142,7 @@ module ActionController #:nodoc:
#
# You can set the variant in a +before_action+:
#
- # request.variant = :tablet if request.user_agent =~ /iPad/
+ # request.variant = :tablet if /iPad/.match?(request.user_agent)
#
# Respond to variants in the action just like you respond to formats:
#
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index 150ae2666c..15c9937405 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -240,7 +240,7 @@ module ActionController
# Performs parameters wrapping upon the request. Called automatically
# by the metal call stack.
- def process_action(*args)
+ def process_action(*)
_perform_parameter_wrapping if _wrapper_enabled?
super
end
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index a251c29d23..660aef4106 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -163,18 +163,18 @@ module ActionController
"/**/#{options[:callback]}(#{json})"
else
- self.content_type ||= Mime[:json]
+ self.content_type = Mime[:json] if media_type.nil?
json
end
end
add :js do |js, options|
- self.content_type ||= Mime[:js]
+ self.content_type = Mime[:js] if media_type.nil?
js.respond_to?(:to_js) ? js.to_js(options) : js
end
add :xml do |xml, options|
- self.content_type ||= Mime[:xml]
+ self.content_type = Mime[:xml] if media_type.nil?
xml.respond_to?(:to_xml) ? xml.to_xml(options) : xml
end
end
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index efa5de313c..fd22c4fa64 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -77,6 +77,10 @@ module ActionController
end
end
+ def _set_vary_header
+ self.headers["Vary"] = "Accept" if request.should_apply_vary_header?
+ end
+
# Normalize arguments by catching blocks and setting them on :update.
def _normalize_args(action = nil, options = {}, &blk)
options = super
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 5a5c04234b..31df6cea0f 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -280,7 +280,7 @@ module ActionController #:nodoc:
# Check for cross-origin JavaScript responses.
def non_xhr_javascript_response? # :doc:
- content_type =~ %r(\A(?:text|application)/javascript) && !request.xhr?
+ %r(\A(?:text|application)/javascript).match?(media_type) && !request.xhr?
end
AUTHENTICITY_TOKEN_LENGTH = 32
diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb
index 44f7fb7a07..59704f2797 100644
--- a/actionpack/lib/action_controller/metal/rescue.rb
+++ b/actionpack/lib/action_controller/metal/rescue.rb
@@ -18,7 +18,7 @@ module ActionController #:nodoc:
end
private
- def process_action(*args)
+ def process_action(*)
super
rescue Exception => exception
request.env["action_dispatch.show_detailed_exceptions"] ||= show_detailed_exceptions?
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 6a07a73d94..4c9eb20c65 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -225,7 +225,7 @@ module ActionController
class << self
def nested_attribute?(key, value) # :nodoc:
- key =~ /\A-?\d+\z/ && (value.is_a?(Hash) || value.is_a?(Parameters))
+ /\A-?\d+\z/.match?(key) && (value.is_a?(Hash) || value.is_a?(Parameters))
end
end
@@ -259,6 +259,11 @@ module ActionController
@parameters == other
end
end
+ alias eql? ==
+
+ def hash
+ [@parameters.hash, @permitted].hash
+ end
# Returns a safe <tt>ActiveSupport::HashWithIndifferentAccess</tt>
# representation of the parameters with all unpermitted keys removed.
@@ -744,6 +749,18 @@ module ActionController
end
alias_method :delete_if, :reject!
+ # Returns a new instance of <tt>ActionController::Parameters</tt> without the blank values.
+ # Uses Object#blank? for determining if a value is blank.
+ def compact_blank
+ reject { |_k, v| v.blank? }
+ end
+
+ # Removes all blank values in place and returns self.
+ # Uses Object#blank? for determining if a value is blank.
+ def compact_blank!
+ reject! { |_k, v| v.blank? }
+ end
+
# Returns values that were assigned to the given +keys+. Note that all the
# +Hash+ objects will be converted to <tt>ActionController::Parameters</tt>.
def values_at(*keys)
diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb
index dadf6d3445..b48c7a1afa 100644
--- a/actionpack/lib/action_controller/renderer.rb
+++ b/actionpack/lib/action_controller/renderer.rb
@@ -65,7 +65,7 @@ module ActionController
def initialize(controller, env, defaults)
@controller = controller
@defaults = defaults
- @env = normalize_keys defaults.merge(env)
+ @env = normalize_keys defaults, env
end
# Render templates with any options from ActionController::Base#render_to_string.
@@ -97,8 +97,9 @@ module ActionController
end
private
- def normalize_keys(env)
+ def normalize_keys(defaults, env)
new_env = {}
+ defaults.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
env.each_pair { |k, v| new_env[rack_key_for(k)] = rack_value_for(k, v) }
new_env["rack.url_scheme"] = new_env["HTTPS"] == "on" ? "https" : "http"
new_env
@@ -120,7 +121,7 @@ module ActionController
}
def rack_key_for(key)
- RACK_KEY_TRANSLATION.fetch(key, key.to_s)
+ RACK_KEY_TRANSLATION[key] || key.to_s
end
def rack_value_for(key, value)
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 47e0099f20..1632ac3ae8 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -593,8 +593,8 @@ module ActionController
private
def scrub_env!(env)
- env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
- env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
+ env.delete_if { |k, v| k.match?(/^(action_dispatch|rack)\.request/) }
+ env.delete_if { |k, v| k.match?(/^action_dispatch\.rescue/) }
env.delete "action_dispatch.request.query_parameters"
env.delete "action_dispatch.request.request_parameters"
env["rack.input"] = StringIO.new
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 6a4ba9af4a..67d303a368 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -43,6 +43,7 @@ module ActionDispatch
eager_autoload do
autoload_under "http" do
autoload :ContentSecurityPolicy
+ autoload :FeaturePolicy
autoload :Request
autoload :Response
end
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 7be30be77a..0258d85564 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -150,8 +150,8 @@ module ActionDispatch
directive, argument = segment.split("=", 2)
if SPECIAL_KEYS.include? directive
- key = directive.tr("-", "_")
- cache_control[key.to_sym] = argument || true
+ directive.tr!("-", "_")
+ cache_control[directive.to_sym] = argument || true
else
cache_control[:extras] ||= []
cache_control[:extras] << segment
diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb
index 5c6fa2dfa7..e8cf1b95a5 100644
--- a/actionpack/lib/action_dispatch/http/content_security_policy.rb
+++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb
@@ -22,8 +22,9 @@ module ActionDispatch #:nodoc:
if policy = request.content_security_policy
nonce = request.content_security_policy_nonce
+ nonce_directives = request.content_security_policy_nonce_directives
context = request.controller_instance || request
- headers[header_name(request)] = policy.build(context, nonce)
+ headers[header_name(request)] = policy.build(context, nonce, nonce_directives)
end
response
@@ -32,7 +33,7 @@ module ActionDispatch #:nodoc:
private
def html_response?(headers)
if content_type = headers[CONTENT_TYPE]
- content_type =~ /html/
+ /html/.match?(content_type)
end
end
@@ -54,6 +55,7 @@ module ActionDispatch #:nodoc:
POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only"
NONCE_GENERATOR = "action_dispatch.content_security_policy_nonce_generator"
NONCE = "action_dispatch.content_security_policy_nonce"
+ NONCE_DIRECTIVES = "action_dispatch.content_security_policy_nonce_directives"
def content_security_policy
get_header(POLICY)
@@ -79,6 +81,14 @@ module ActionDispatch #:nodoc:
set_header(NONCE_GENERATOR, generator)
end
+ def content_security_policy_nonce_directives
+ get_header(NONCE_DIRECTIVES)
+ end
+
+ def content_security_policy_nonce_directives=(generator)
+ set_header(NONCE_DIRECTIVES, generator)
+ end
+
def content_security_policy_nonce
if content_security_policy_nonce_generator
if nonce = get_header(NONCE)
@@ -127,13 +137,17 @@ module ActionDispatch #:nodoc:
object_src: "object-src",
prefetch_src: "prefetch-src",
script_src: "script-src",
+ script_src_attr: "script-src-attr",
+ script_src_elem: "script-src-elem",
style_src: "style-src",
+ style_src_attr: "style-src-attr",
+ style_src_elem: "style-src-elem",
worker_src: "worker-src"
}.freeze
- NONCE_DIRECTIVES = %w[script-src style-src].freeze
+ DEFAULT_NONCE_DIRECTIVES = %w[script-src style-src].freeze
- private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES
+ private_constant :MAPPINGS, :DIRECTIVES, :DEFAULT_NONCE_DIRECTIVES
attr_reader :directives
@@ -202,8 +216,9 @@ module ActionDispatch #:nodoc:
end
end
- def build(context = nil, nonce = nil)
- build_directives(context, nonce).compact.join("; ")
+ def build(context = nil, nonce = nil, nonce_directives = nil)
+ nonce_directives = DEFAULT_NONCE_DIRECTIVES if nonce_directives.nil?
+ build_directives(context, nonce, nonce_directives).compact.join("; ")
end
private
@@ -226,10 +241,10 @@ module ActionDispatch #:nodoc:
end
end
- def build_directives(context, nonce)
+ def build_directives(context, nonce, nonce_directives)
@directives.map do |directive, sources|
if sources.is_a?(Array)
- if nonce && nonce_directive?(directive)
+ if nonce && nonce_directive?(directive, nonce_directives)
"#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'"
else
"#{directive} #{build_directive(sources, context).join(' ')}"
@@ -264,8 +279,8 @@ module ActionDispatch #:nodoc:
end
end
- def nonce_directive?(directive)
- NONCE_DIRECTIVES.include?(directive)
+ def nonce_directive?(directive, nonce_directives)
+ nonce_directives.include?(directive)
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/feature_policy.rb b/actionpack/lib/action_dispatch/http/feature_policy.rb
new file mode 100644
index 0000000000..c09690f9fc
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/feature_policy.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/object/deep_dup"
+
+module ActionDispatch #:nodoc:
+ class FeaturePolicy
+ class Middleware
+ CONTENT_TYPE = "Content-Type"
+ POLICY = "Feature-Policy"
+
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ request = ActionDispatch::Request.new(env)
+ _, headers, _ = response = @app.call(env)
+
+ return response unless html_response?(headers)
+ return response if policy_present?(headers)
+
+ if policy = request.feature_policy
+ headers[POLICY] = policy.build(request.controller_instance)
+ end
+
+ if policy_empty?(policy)
+ headers.delete(POLICY)
+ end
+
+ response
+ end
+
+ private
+ def html_response?(headers)
+ if content_type = headers[CONTENT_TYPE]
+ /html/.match?(content_type)
+ end
+ end
+
+ def policy_present?(headers)
+ headers[POLICY]
+ end
+
+ def policy_empty?(policy)
+ policy&.directives&.empty?
+ end
+ end
+
+ module Request
+ POLICY = "action_dispatch.feature_policy"
+
+ def feature_policy
+ get_header(POLICY)
+ end
+
+ def feature_policy=(policy)
+ set_header(POLICY, policy)
+ end
+ end
+
+ MAPPINGS = {
+ self: "'self'",
+ none: "'none'",
+ }.freeze
+
+ # List of available features can be found at
+ # https://github.com/WICG/feature-policy/blob/master/features.md#policy-controlled-features
+ DIRECTIVES = {
+ accelerometer: "accelerometer",
+ ambient_light_sensor: "ambient-light-sensor",
+ autoplay: "autoplay",
+ camera: "camera",
+ encrypted_media: "encrypted-media",
+ fullscreen: "fullscreen",
+ geolocation: "geolocation",
+ gyroscope: "gyroscope",
+ magnetometer: "magnetometer",
+ microphone: "microphone",
+ midi: "midi",
+ payment: "payment",
+ picture_in_picture: "picture-in-picture",
+ speaker: "speaker",
+ usb: "usb",
+ vibrate: "vibrate",
+ vr: "vr",
+ }.freeze
+
+ private_constant :MAPPINGS, :DIRECTIVES
+
+ attr_reader :directives
+
+ def initialize
+ @directives = {}
+ yield self if block_given?
+ end
+
+ def initialize_copy(other)
+ @directives = other.directives.deep_dup
+ end
+
+ DIRECTIVES.each do |name, directive|
+ define_method(name) do |*sources|
+ if sources.first
+ @directives[directive] = apply_mappings(sources)
+ else
+ @directives.delete(directive)
+ end
+ end
+ end
+
+ def build(context = nil)
+ build_directives(context).compact.join("; ")
+ end
+
+ private
+ def apply_mappings(sources)
+ sources.map do |source|
+ case source
+ when Symbol
+ apply_mapping(source)
+ when String, Proc
+ source
+ else
+ raise ArgumentError, "Invalid HTTP feature policy source: #{source.inspect}"
+ end
+ end
+ end
+
+ def apply_mapping(source)
+ MAPPINGS.fetch(source) do
+ raise ArgumentError, "Unknown HTTP feature policy source mapping: #{source.inspect}"
+ end
+ end
+
+ def build_directives(context)
+ @directives.map do |directive, sources|
+ if sources.is_a?(Array)
+ "#{directive} #{build_directive(sources, context).join(' ')}"
+ elsif sources
+ directive
+ else
+ nil
+ end
+ end
+ end
+
+ def build_directive(sources, context)
+ sources.map { |source| resolve_source(source, context) }
+ end
+
+ def resolve_source(source, context)
+ case source
+ when String
+ source
+ when Symbol
+ source.to_s
+ when Proc
+ if context.nil?
+ raise RuntimeError, "Missing context for the dynamic feature policy source: #{source.inspect}"
+ else
+ context.instance_exec(&source)
+ end
+ else
+ raise RuntimeError, "Unexpected feature policy source: #{source.inspect}"
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 7a7a493f64..7ad1ba3e0e 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -23,7 +23,7 @@ module ActionDispatch
# change { file: { code: "xxxx"} }
#
# env["action_dispatch.parameter_filter"] = -> (k, v) do
- # v.reverse! if k =~ /secret/i
+ # v.reverse! if k.match?(/secret/i)
# end
# => reverses the value to all keys matching /secret/i
module FilterParameters
diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb
index d780d5f793..3bd1f5109d 100644
--- a/actionpack/lib/action_dispatch/http/filter_redirect.rb
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -27,7 +27,7 @@ module ActionDispatch
if String === filter
location.include?(filter)
elsif Regexp === filter
- location =~ filter
+ location.match?(filter)
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index a2cac49082..6bf4e652d3 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -62,13 +62,7 @@ module ActionDispatch
def formats
fetch_header("action_dispatch.request.formats") do |k|
- params_readable = begin
- parameters[:format]
- rescue *RESCUABLE_MIME_FORMAT_ERRORS
- false
- end
-
- v = if params_readable
+ v = if params_readable?
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
@@ -153,12 +147,22 @@ module ActionDispatch
order.include?(Mime::ALL) ? format : nil
end
+ def should_apply_vary_header?
+ !params_readable? && use_accept_header && valid_accept_header
+ end
+
private
BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
+ def params_readable? # :doc:
+ parameters[:format]
+ rescue *RESCUABLE_MIME_FORMAT_ERRORS
+ false
+ end
+
def valid_accept_header # :doc:
(xhr? && (accept.present? || content_mime_type)) ||
- (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
+ (accept.present? && !accept.match?(BROWSER_LIKE_ACCEPTS))
end
def use_accept_header # :doc:
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index ed1d50f3b9..60b78c0582 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -202,7 +202,7 @@ module Mime
# For an input of <tt>'application'</tt>, returns <tt>[Mime[:html], Mime[:js],
# Mime[:xml], Mime[:yaml], Mime[:atom], Mime[:json], Mime[:rss], Mime[:url_encoded_form]</tt>.
def parse_data_with_trailing_star(type)
- Mime::SET.select { |m| m =~ type }
+ Mime::SET.select { |m| m.match?(type) }
end
# This method is opposite of register method.
@@ -283,8 +283,14 @@ module Mime
@synonyms.any? { |synonym| synonym.to_s =~ regexp } || @string =~ regexp
end
+ def match?(mime_type)
+ return false unless mime_type
+ regexp = Regexp.new(Regexp.quote(mime_type.to_s))
+ @synonyms.any? { |synonym| synonym.to_s.match?(regexp) } || @string.match?(regexp)
+ end
+
def html?
- symbol == :html || @string =~ /html/
+ (symbol == :html) || /html/.match?(@string)
end
def all?; false; end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 44f23940d3..54dbb536c1 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -23,6 +23,7 @@ module ActionDispatch
include ActionDispatch::Http::FilterParameters
include ActionDispatch::Http::URL
include ActionDispatch::ContentSecurityPolicy::Request
+ include ActionDispatch::FeaturePolicy::Request
include Rack::Request::Env
autoload :Session, "action_dispatch/request/session"
@@ -84,7 +85,7 @@ module ActionDispatch
def controller_class_for(name)
if name
controller_param = name.underscore
- const_name = "#{controller_param.camelize}Controller"
+ const_name = controller_param.camelize << "Controller"
ActiveSupport::Dependencies.constantize(const_name)
else
PASS_NOT_FOUND
@@ -264,7 +265,7 @@ module ActionDispatch
# (case-insensitive), which may need to be manually added depending on the
# choice of JavaScript libraries and frameworks.
def xml_http_request?
- get_header("HTTP_X_REQUESTED_WITH") =~ /XMLHttpRequest/i
+ /XMLHttpRequest/i.match?(get_header("HTTP_X_REQUESTED_WITH"))
end
alias :xhr? :xml_http_request?
@@ -399,7 +400,7 @@ module ActionDispatch
# True if the request came from localhost, 127.0.0.1, or ::1.
def local?
- LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
+ LOCALHOST.match?(remote_addr) && LOCALHOST.match?(remote_ip)
end
def request_parameters=(params)
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 3b0f6378ea..225ae0a497 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -133,7 +133,7 @@ module ActionDispatch
end
def named_host?(host)
- IP_HOST_REGEXP !~ host
+ !IP_HOST_REGEXP.match?(host)
end
def normalize_protocol(protocol)
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 4aee7a6f83..9184676801 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -148,7 +148,7 @@ module ActionDispatch
end
def glob?
- !path.spec.grep(Nodes::Star).empty?
+ path.spec.any?(Nodes::Star)
end
def dispatcher?
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 1f3bf7fca6..9d94d94ffb 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -346,28 +346,6 @@ module ActionDispatch
@cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
end
- def handle_options(options) # :nodoc:
- if options[:expires].respond_to?(:from_now)
- options[:expires] = options[:expires].from_now
- end
-
- options[:path] ||= "/"
-
- if options[:domain] == :all || options[:domain] == "all"
- # If there is a provided tld length then we use it otherwise default domain regexp.
- domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
-
- # If host is not ip and matches domain regexp.
- # (ip confirms to domain regexp so we explicitly check for ip)
- options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
- ".#{$&}"
- end
- elsif options[:domain].is_a? Array
- # If host matches one of the supplied domains without a dot in front of it.
- options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
- end
- end
-
# Sets the cookie named +name+. The second argument may be the cookie's
# value or a hash of options as documented above.
def []=(name, options)
@@ -447,6 +425,28 @@ module ActionDispatch
def write_cookie?(cookie)
request.ssl? || !cookie[:secure] || always_write_cookie
end
+
+ def handle_options(options)
+ if options[:expires].respond_to?(:from_now)
+ options[:expires] = options[:expires].from_now
+ end
+
+ options[:path] ||= "/"
+
+ if options[:domain] == :all || options[:domain] == "all"
+ # If there is a provided tld length then we use it otherwise default domain regexp.
+ domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
+
+ # If host is not ip and matches domain regexp.
+ # (ip confirms to domain regexp so we explicitly check for ip)
+ options[:domain] = if !request.host.match?(/^[\d.]+$/) && (request.host =~ domain_regexp)
+ ".#{$&}"
+ end
+ elsif options[:domain].is_a? Array
+ # If host matches one of the supplied domains without a dot in front of it.
+ options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
+ end
+ end
end
class AbstractCookieJar # :nodoc:
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index f8937a2faf..e546d1c11f 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -137,9 +137,7 @@ module ActionDispatch
return unless logger
exception = wrapper.exception
-
- trace = wrapper.application_trace
- trace = wrapper.framework_trace if trace.empty?
+ trace = wrapper.exception_trace
ActiveSupport::Deprecation.silence do
message = []
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 2da0ef9600..e4a2a51c57 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -36,18 +36,23 @@ module ActionDispatch
"ActionView::Template::Error"
]
+ cattr_accessor :silent_exceptions, default: [
+ "ActionController::RoutingError"
+ ]
+
attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
def initialize(backtrace_cleaner, exception)
@backtrace_cleaner = backtrace_cleaner
@exception = exception
+ @exception_class_name = @exception.class.name
@wrapped_causes = wrapped_causes_for(exception, backtrace_cleaner)
expand_backtrace if exception.is_a?(SyntaxError) || exception.cause.is_a?(SyntaxError)
end
def unwrapped_exception
- if wrapper_exceptions.include?(exception.class.to_s)
+ if wrapper_exceptions.include?(@exception_class_name)
exception.cause
else
exception
@@ -55,13 +60,19 @@ module ActionDispatch
end
def rescue_template
- @@rescue_templates[@exception.class.name]
+ @@rescue_templates[@exception_class_name]
end
def status_code
self.class.status_code_for_exception(unwrapped_exception.class.name)
end
+ def exception_trace
+ trace = application_trace
+ trace = framework_trace if trace.empty? && !silent_exceptions.include?(@exception_class_name)
+ trace
+ end
+
def application_trace
clean_backtrace(:silent)
end
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 00902ede21..237eccf45f 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -13,7 +13,7 @@ module ActionDispatch
#
# Requests can opt-out of redirection with +exclude+:
#
- # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
+ # config.ssl_options = { redirect: { exclude: -> request { /healthcheck/.match?(request.path) } } }
#
# Cookies will not be flagged as secure for excluded requests.
#
@@ -126,7 +126,7 @@ module ActionDispatch
[ @redirect.fetch(:status, redirection_status(request)),
{ "Content-Type" => "text/html",
"Location" => https_location_for(request) },
- @redirect.fetch(:body, []) ]
+ (@redirect[:body] || []) ]
end
def redirection_status(request)
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index 1f2f7757a3..eddcdbaeac 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -32,18 +32,13 @@ module ActionDispatch
return false unless ::Rack::Utils.valid_path? path
path = ::Rack::Utils.clean_path_info path
- paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"]
+ return ::Rack::Utils.escape_path(path).b if file_readable?(path)
- if match = paths.detect { |p|
- path = File.join(@root, p.b)
- begin
- File.file?(path) && File.readable?(path)
- rescue SystemCallError
- false
- end
- }
- return ::Rack::Utils.escape_path(match).b
- end
+ path_with_ext = path + ext
+ return ::Rack::Utils.escape_path(path_with_ext).b if file_readable?(path_with_ext)
+
+ path << "/" << @index << ext
+ return ::Rack::Utils.escape_path(path).b if file_readable?(path)
end
def call(env)
@@ -83,11 +78,11 @@ module ActionDispatch
end
def gzip_encoding_accepted?(request)
- request.accept_encoding.any? { |enc, quality| enc =~ /\bgzip\b/i }
+ request.accept_encoding.any? { |enc, quality| /\bgzip\b/i.match?(enc) }
end
def gzip_file_path(path)
- can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
+ can_gzip_mime = /\A(?:text\/|application\/javascript)/.match?(content_type(path))
gzip_path = "#{path}.gz"
if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape_path(gzip_path)))
gzip_path
@@ -95,6 +90,12 @@ module ActionDispatch
false
end
end
+
+ def file_readable?(path)
+ file_path = File.join(@root, path.b)
+ File.file?(file_path) && File.readable?(file_path)
+ rescue SystemCallError
+ end
end
# This middleware will attempt to return the contents of a file's body from
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 66f90980b9..2e09aed41d 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -54,11 +54,5 @@ module ActionDispatch
ActionDispatch.test_app = app
end
-
- initializer "action_dispatch.system_tests" do |app|
- ActiveSupport.on_load(:action_dispatch_system_test_case) do
- include app.routes.url_helpers
- end
- end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index 6e40a18009..bf286c299d 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -94,7 +94,7 @@ module ActionDispatch
if filter
@routes.select do |route|
route_wrapper = RouteWrapper.new(route)
- filter.any? { |default, value| route_wrapper.send(default) =~ value }
+ filter.any? { |default, value| value.match?(route_wrapper.send(default)) }
end
else
@routes
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index d1100089b1..da95e14cbb 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -112,7 +112,7 @@ module ActionDispatch
end
def self.optional_format?(path, format)
- format != false && path !~ OPTIONAL_FORMAT_REGEX
+ format != false && !path.match?(OPTIONAL_FORMAT_REGEX)
end
def initialize(set:, ast:, controller:, default_action:, to:, formatted:, via:, options_constraints:, anchor:, scope_params:, options:)
@@ -367,7 +367,7 @@ module ActionDispatch
def translate_controller(controller)
return controller if Regexp === controller
- return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
+ return controller.to_s if /\A[a-z_0-9][a-z_0-9\/]*\z/.match?(controller)
yield
end
@@ -403,7 +403,7 @@ module ActionDispatch
# for root cases, where the latter is the correct one.
def self.normalize_path(path)
path = Journey::Router::Utils.normalize_path(path)
- path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/(\(+[^)]+\)){1,}$}
+ path.gsub!(%r{/(\(+)/?}, '\1/') unless %r{^/(\(+[^)]+\)){1,}$}.match?(path)
path
end
@@ -996,7 +996,7 @@ module ActionDispatch
#
# Requests to routes can be constrained based on specific criteria:
#
- # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do
+ # constraints(-> (req) { /iPhone/.match?(req.env["HTTP_USER_AGENT"]) }) do
# resources :iphones
# end
#
@@ -1006,7 +1006,7 @@ module ActionDispatch
#
# class Iphone
# def self.matches?(request)
- # request.env["HTTP_USER_AGENT"] =~ /iPhone/
+ # /iPhone/.match?(request.env["HTTP_USER_AGENT"])
# end
# end
#
@@ -1833,7 +1833,7 @@ module ActionDispatch
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
- candidate unless candidate !~ /\A[_a-z]/i || has_named_route?(candidate)
+ candidate unless !candidate.match?(/\A[_a-z]/i) || has_named_route?(candidate)
else
candidate
end
@@ -1887,7 +1887,7 @@ module ActionDispatch
options_constraints = options.delete(:constraints) || {}
path_types = paths.group_by(&:class)
- path_types.fetch(String, []).each do |_path|
+ (path_types[String] || []).each do |_path|
route_options = options.dup
if _path && option_path
raise ArgumentError, "Ambiguous route definition. Both :path and the route path were specified as strings."
@@ -1896,7 +1896,7 @@ module ActionDispatch
decomposed_match(_path, controller, route_options, _path, to, via, formatted, anchor, options_constraints)
end
- path_types.fetch(Symbol, []).each do |action|
+ (path_types[Symbol] || []).each do |action|
route_options = options.dup
decomposed_match(action, controller, route_options, option_path, to, via, formatted, anchor, options_constraints)
end
@@ -1916,7 +1916,7 @@ module ActionDispatch
end
def using_match_shorthand?(path)
- path =~ %r{^/?[-\w]+/[-\w/]+$}
+ %r{^/?[-\w]+/[-\w/]+$}.match?(path)
end
def decomposed_match(path, controller, options, _path, to, via, formatted, anchor, options_constraints)
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 5b35b68c44..db8c54ba84 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -836,7 +836,7 @@ module ActionDispatch
def recognize_path(path, environment = {})
method = (environment[:method] || "GET").to_s.upcase
- path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://}
+ path = Journey::Router::Utils.normalize_path(path) unless %r{://}.match?(path)
extras = environment[:extras] || {}
begin
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
index a7fb5fa330..aae96975c7 100644
--- a/actionpack/lib/action_dispatch/system_test_case.rb
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -1,9 +1,10 @@
# frozen_string_literal: true
-gem "capybara", ">= 2.15"
+gem "capybara", ">= 3.26"
require "capybara/dsl"
require "capybara/minitest"
+require "selenium/webdriver"
require "action_controller"
require "action_dispatch/system_testing/driver"
require "action_dispatch/system_testing/browser"
@@ -158,12 +159,33 @@ module ActionDispatch
driven_by :selenium
- def url_options # :nodoc:
- default_url_options.merge(host: Capybara.app_host)
- end
+ private
+ def url_helpers
+ @url_helpers ||=
+ if ActionDispatch.test_app
+ Class.new do
+ include ActionDispatch.test_app.routes.url_helpers
- ActiveSupport.run_load_hooks(:action_dispatch_system_test_case, self)
- end
+ def url_options
+ default_url_options.reverse_merge(host: Capybara.app_host || Capybara.current_session.server_url)
+ end
+ end.new
+ end
+ end
- SystemTestCase.start_application
+ def method_missing(name, *args, &block)
+ if url_helpers.respond_to?(name)
+ url_helpers.public_send(name, *args, &block)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(name, include_private = false)
+ url_helpers.respond_to?(name)
+ end
+ end
end
+
+ActiveSupport.run_load_hooks :action_dispatch_system_test_case, ActionDispatch::SystemTestCase
+ActionDispatch::SystemTestCase.start_application
diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb
index c34907b6cb..91f332be13 100644
--- a/actionpack/lib/action_dispatch/system_testing/browser.rb
+++ b/actionpack/lib/action_dispatch/system_testing/browser.rb
@@ -39,6 +39,29 @@ module ActionDispatch
end
end
+ # driver_path can be configured as a proc. The webdrivers gem uses this
+ # proc to update web drivers. Running this proc early allows us to only
+ # update the webdriver once and avoid race conditions when using
+ # parallel tests.
+ def preload
+ case type
+ when :chrome
+ if ::Selenium::WebDriver::Service.respond_to? :driver_path=
+ ::Selenium::WebDriver::Chrome::Service.driver_path&.call
+ else
+ # Selenium <= v3.141.0
+ ::Selenium::WebDriver::Chrome.driver_path
+ end
+ when :firefox
+ if ::Selenium::WebDriver::Service.respond_to? :driver_path=
+ ::Selenium::WebDriver::Firefox::Service.driver_path&.call
+ else
+ # Selenium <= v3.141.0
+ ::Selenium::WebDriver::Firefox.driver_path
+ end
+ end
+ end
+
private
def headless_chrome_browser_options
capabilities.args << "--headless"
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
index 25a09dd918..15943a55ea 100644
--- a/actionpack/lib/action_dispatch/system_testing/driver.rb
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -9,6 +9,8 @@ module ActionDispatch
@screen_size = options[:screen_size]
@options = options[:options]
@capabilities = capabilities
+
+ @browser.preload
end
def use
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 056ce51a61..cba053ee4c 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
@@ -9,10 +9,16 @@ module ActionDispatch
#
# +take_screenshot+ can be used at any point in your system tests to take
# a screenshot of the current state. This can be useful for debugging or
- # automating visual testing.
+ # automating visual testing. You can take multiple screenshots per test
+ # to investigate changes at different points during your test. These will be
+ # named with a sequential prefix (or 'failed' for failing tests)
#
# The screenshot will be displayed in your console, if supported.
#
+ # You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT_HTML+ environment variable to
+ # save the HTML from the page that is being screenhoted so you can investigate the
+ # elements on the page at the time of the screenshot
+ #
# You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to
# control the output. Possible values are:
# * [+simple+ (default)] Only displays the screenshot path.
@@ -22,6 +28,8 @@ module ActionDispatch
# * [+artifact+] Display the screenshot in the terminal, using the terminal
# artifact format (https://buildkite.github.io/terminal-to-html/inline-images/).
def take_screenshot
+ increment_unique
+ save_html if save_html?
save_image
puts display_image
end
@@ -38,17 +46,48 @@ module ActionDispatch
end
private
+ attr_accessor :_screenshot_counter
+
+ def save_html?
+ ENV["RAILS_SYSTEM_TESTING_SCREENSHOT_HTML"] == "1"
+ end
+
+ def increment_unique
+ @_screenshot_counter ||= 0
+ @_screenshot_counter += 1
+ end
+
+ def unique
+ failed? ? "failures" : (_screenshot_counter || 0).to_s
+ end
+
def image_name
- name = method_name[0...225]
- failed? ? "failures_#{name}" : name
+ name = "#{unique}_#{method_name}"
+ name[0...225]
end
def image_path
- @image_path ||= absolute_image_path.to_s
+ absolute_image_path.to_s
+ end
+
+ def html_path
+ absolute_html_path.to_s
+ end
+
+ def absolute_path
+ Rails.root.join("tmp/screenshots/#{image_name}")
end
def absolute_image_path
- Rails.root.join("tmp/screenshots/#{image_name}.png")
+ "#{absolute_path}.png"
+ end
+
+ def absolute_html_path
+ "#{absolute_path}.html"
+ end
+
+ def save_html
+ page.save_page(absolute_html_path)
end
def save_image
@@ -66,7 +105,8 @@ module ActionDispatch
end
def display_image
- message = +"[Screenshot]: #{image_path}\n"
+ message = +"[Screenshot Image]: #{image_path}\n"
+ message << +"[Screenshot HTML]: #{html_path}\n" if save_html?
case output_type
when "artifact"
diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
index 20f6a7634f..30dc21ebb9 100644
--- a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
+++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb
@@ -4,15 +4,12 @@ module ActionDispatch
module SystemTesting
module TestHelpers
module SetupAndTeardown # :nodoc:
- DEFAULT_HOST = "http://127.0.0.1"
-
def host!(host)
- Capybara.app_host = host
- end
+ ActiveSupport::Deprecation.warn \
+ "ActionDispatch::SystemTestCase#host! is deprecated with no replacement. " \
+ "Set Capybara.app_host directly or rely on Capybara's default host."
- def before_setup
- host! DEFAULT_HOST
- super
+ Capybara.app_host = host
end
def before_teardown
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index bb8b43ad4d..9e7b4301a8 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -3,7 +3,6 @@
require "stringio"
require "uri"
require "active_support/core_ext/kernel/singleton_class"
-require "active_support/core_ext/object/try"
require "rack/test"
require "minitest"
@@ -50,11 +49,16 @@ module ActionDispatch
# Follow a single redirect response. If the last response was not a
# redirect, an exception will be raised. Otherwise, the redirect is
- # performed on the location header. Any arguments are passed to the
- # underlying call to `get`.
+ # performed on the location header. If the redirection is a 307 redirect,
+ # the same HTTP verb will be used when redirecting, otherwise a GET request
+ # will be performed. Any arguments are passed to the
+ # underlying request.
def follow_redirect!(**args)
raise "not a redirect! #{status} #{status_message}" unless redirect?
- get(response.location, **args)
+
+ method = response.status == 307 ? request.method.downcase : :get
+ public_send(method, response.location, **args)
+
status
end
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index cce229b30d..4f5f5b71ae 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -213,6 +213,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
redirect_to action_url("get")
end
+ def redirect_307
+ redirect_to action_url("post"), status: 307
+ end
+
def remove_header
response.headers.delete params[:header]
head :ok, "c" => "3"
@@ -337,6 +341,15 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
+ def test_307_redirect_uses_the_same_http_verb
+ with_test_route_set do
+ post "/redirect_307"
+ assert_equal 307, status
+ follow_redirect!
+ assert_equal "POST", request.method
+ end
+ end
+
def test_redirect_reset_html_document
with_test_route_set do
get "/redirect"
@@ -530,6 +543,32 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
end
+ def test_setting_vary_header_when_request_is_xhr_with_accept_header
+ with_test_route_set do
+ get "/get", headers: { "Accept" => "application/json" }, xhr: true
+ assert_equal "Accept", response.headers["Vary"]
+ end
+ end
+
+ def test_not_setting_vary_header_when_format_is_provided
+ with_test_route_set do
+ get "/get", params: { format: "json" }
+ assert_nil response.headers["Vary"]
+ end
+ end
+
+ def test_not_setting_vary_header_when_ignore_accept_header_is_set
+ original_ignore_accept_header = ActionDispatch::Request.ignore_accept_header
+ ActionDispatch::Request.ignore_accept_header = true
+
+ with_test_route_set do
+ get "/get", headers: { "Accept" => "application/json" }, xhr: true
+ assert_nil response.headers["Vary"]
+ end
+ ensure
+ ActionDispatch::Request.ignore_accept_header = original_ignore_accept_header
+ end
+
private
def with_default_headers(headers)
original = ActionDispatch::Response.default_headers
@@ -567,7 +606,7 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest
class Poller
def self.call(env)
- if env["PATH_INFO"] =~ /^\/success/
+ if /^\/success/.match?(env["PATH_INFO"])
[200, { "Content-Type" => "text/plain", "Content-Length" => "12" }, ["Hello World!"]]
else
[404, { "Content-Type" => "text/plain", "Content-Length" => "0" }, []]
diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb
index 1a7e7f6cbb..46ab7b4f74 100644
--- a/actionpack/test/controller/log_subscriber_test.rb
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -139,7 +139,7 @@ class ACLogSubscriberTest < ActionController::TestCase
def test_process_action_without_parameters
get :show
wait
- assert_nil logs.detect { |l| l =~ /Parameters/ }
+ assert_nil logs.detect { |l| /Parameters/.match?(l) }
end
def test_process_action_with_parameters
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
index cb49eeb1b7..3d1538ff64 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -284,12 +284,14 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
params1 = ActionController::Parameters.new(a: 1, b: 2)
params2 = ActionController::Parameters.new(a: 1, b: 2)
assert(params1 == params2)
+ assert(params1.hash == params2.hash)
end
test "is equal to Parameters instance with same permitted params" do
params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
assert(params1 == params2)
+ assert(params1.hash == params2.hash)
end
test "is equal to Parameters instance with same different source params, but same permitted params" do
@@ -297,6 +299,8 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a)
assert(params1 == params2)
assert(params2 == params1)
+ assert(params1.hash == params2.hash)
+ assert(params2.hash == params1.hash)
end
test "is not equal to an unpermitted Parameters instance with same params" do
@@ -304,6 +308,8 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
params2 = ActionController::Parameters.new(a: 1)
assert(params1 != params2)
assert(params2 != params1)
+ assert(params1.hash != params2.hash)
+ assert(params2.hash != params1.hash)
end
test "is not equal to Parameters instance with different permitted params" do
@@ -311,6 +317,8 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
assert(params1 != params2)
assert(params2 != params1)
+ assert(params1.hash != params2.hash)
+ assert(params2.hash != params1.hash)
end
test "equality with simple types works" do
diff --git a/actionpack/test/controller/parameters/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb
index 31ee7964f5..ffffd2996f 100644
--- a/actionpack/test/controller/parameters/mutators_test.rb
+++ b/actionpack/test/controller/parameters/mutators_test.rb
@@ -127,4 +127,22 @@ class ParametersMutatorsTest < ActiveSupport::TestCase
test "deep_transform_keys! retains unpermitted status" do
assert_not_predicate @params.deep_transform_keys! { |k| k }, :permitted?
end
+
+ test "compact_blank retains permitted status" do
+ @params.permit!
+ assert_predicate @params.compact_blank, :permitted?
+ end
+
+ test "compact_blank retains unpermitted status" do
+ assert_not_predicate @params.compact_blank, :permitted?
+ end
+
+ test "compact_blank! retains permitted status" do
+ @params.permit!
+ assert_predicate @params.compact_blank!, :permitted?
+ end
+
+ test "compact_blank! retains unpermitted status" do
+ assert_not_predicate @params.compact_blank!, :permitted?
+ end
end
diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb
index da8f6e8062..02bc0c6ade 100644
--- a/actionpack/test/controller/render_js_test.rb
+++ b/actionpack/test/controller/render_js_test.rb
@@ -32,4 +32,13 @@ class RenderJSTest < ActionController::TestCase
get :show_partial, format: "js", xhr: true
assert_equal "partial js", @response.body
end
+
+ def test_should_not_trigger_content_type_deprecation
+ original = ActionDispatch::Response.return_only_media_type_on_content_type
+ ActionDispatch::Response.return_only_media_type_on_content_type = true
+
+ assert_not_deprecated { get :render_vanilla_js_hello, xhr: true }
+ ensure
+ ActionDispatch::Response.return_only_media_type_on_content_type = original
+ end
end
diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb
index 82c6aaafe5..0045d2be34 100644
--- a/actionpack/test/controller/render_json_test.rb
+++ b/actionpack/test/controller/render_json_test.rb
@@ -133,4 +133,22 @@ class RenderJsonTest < ActionController::TestCase
get :render_json_without_options
assert_equal '{"a":"b"}', @response.body
end
+
+ def test_should_not_trigger_content_type_deprecation
+ original = ActionDispatch::Response.return_only_media_type_on_content_type
+ ActionDispatch::Response.return_only_media_type_on_content_type = true
+
+ assert_not_deprecated { get :render_json_hello_world }
+ ensure
+ ActionDispatch::Response.return_only_media_type_on_content_type = original
+ end
+
+ def test_should_not_trigger_content_type_deprecation_with_callback
+ original = ActionDispatch::Response.return_only_media_type_on_content_type
+ ActionDispatch::Response.return_only_media_type_on_content_type = true
+
+ assert_not_deprecated { get :render_json_hello_world_with_callback, xhr: true }
+ ensure
+ ActionDispatch::Response.return_only_media_type_on_content_type = original
+ end
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index a2a6c69dd3..dd76824413 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -4,6 +4,11 @@ require "abstract_unit"
require "controller/fake_models"
class TestControllerWithExtraEtags < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "test/with_implicit_template.erb" => "Hello explicitly!",
+ "test/hello_world.erb" => "Hello world!"
+ )]
+
def self.controller_name; "test"; end
def self.controller_path; "test"; end
@@ -37,6 +42,11 @@ class TestControllerWithExtraEtags < ActionController::Base
end
class ImplicitRenderTestController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "implicit_render_test/hello_world.erb" => "Hello world!",
+ "implicit_render_test/empty_action_with_template.html.erb" => "<h1>Empty action rendered this implicitly.</h1>\n"
+ )]
+
def empty_action
end
@@ -46,6 +56,10 @@ end
module Namespaced
class ImplicitRenderTestController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "namespaced/implicit_render_test/hello_world.erb" => "Hello world!"
+ )]
+
def hello_world
fresh_when(etag: "abc")
end
@@ -293,13 +307,15 @@ end
module TemplateModificationHelper
private
def modify_template(name)
- path = File.expand_path("../fixtures/#{name}.erb", __dir__)
- original = File.read(path)
- File.write(path, "#{original} Modified!")
+ hash = @controller.view_paths.first.instance_variable_get(:@hash)
+ key = name + ".erb"
+ original = hash[key]
+ hash[key] = "#{original} Modified!"
ActionView::LookupContext::DetailsKey.clear
yield
ensure
- File.write(path, original)
+ hash[key] = original
+ ActionView::LookupContext::DetailsKey.clear
end
end
diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb
index 28d8e281ab..16da41e7c7 100644
--- a/actionpack/test/controller/render_xml_test.rb
+++ b/actionpack/test/controller/render_xml_test.rb
@@ -98,4 +98,13 @@ class RenderXmlTest < ActionController::TestCase
get :implicit_content_type, format: "atom"
assert_equal Mime[:atom], @response.media_type
end
+
+ def test_should_not_trigger_content_type_deprecation
+ original = ActionDispatch::Response.return_only_media_type_on_content_type
+ ActionDispatch::Response.return_only_media_type_on_content_type = true
+
+ assert_not_deprecated { get :render_with_to_xml }
+ ensure
+ ActionDispatch::Response.return_only_media_type_on_content_type = original
+ end
end
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 01250880f5..145d04f5be 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -591,6 +591,15 @@ module RequestForgeryProtectionTests
end
end
+ def test_should_not_trigger_content_type_deprecation
+ original = ActionDispatch::Response.return_only_media_type_on_content_type
+ ActionDispatch::Response.return_only_media_type_on_content_type = true
+
+ assert_not_deprecated { get :same_origin_js, xhr: true }
+ ensure
+ ActionDispatch::Response.return_only_media_type_on_content_type = original
+ end
+
def test_should_not_raise_error_if_token_is_not_a_string
assert_blocked do
patch :index, params: { custom_authenticity_token: { foo: "bar" } }
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 1e42a3a90d..339025ec52 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "abstract_unit"
-require "active_support/core_ext/object/try"
require "active_support/core_ext/object/with_options"
require "active_support/core_ext/array/extract_options"
@@ -1249,7 +1248,7 @@ class ResourcesTest < ActionController::TestCase
shallow_path = "/#{options[:shallow] ? options[:namespace] : options[:path_prefix]}#{path}"
full_path = "/#{options[:path_prefix]}#{path}"
name_prefix = options[:name_prefix]
- shallow_prefix = options[:shallow] ? options[:namespace].try(:gsub, /\//, "_") : options[:name_prefix]
+ shallow_prefix = options[:shallow] ? options[:namespace]&.gsub(/\//, "_") : options[:name_prefix]
new_action = "new"
edit_action = "edit"
diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb
index 30c340ae9e..3d60dc1661 100644
--- a/actionpack/test/dispatch/content_security_policy_test.rb
+++ b/actionpack/test/dispatch/content_security_policy_test.rb
@@ -128,12 +128,36 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase
@policy.script_src false
assert_no_match %r{script-src}, @policy.build
+ @policy.script_src_attr :self
+ assert_match %r{script-src-attr 'self'}, @policy.build
+
+ @policy.script_src_attr false
+ assert_no_match %r{script-src-attr}, @policy.build
+
+ @policy.script_src_elem :self
+ assert_match %r{script-src-elem 'self'}, @policy.build
+
+ @policy.script_src_elem false
+ assert_no_match %r{script-src-elem}, @policy.build
+
@policy.style_src :self
assert_match %r{style-src 'self'}, @policy.build
@policy.style_src false
assert_no_match %r{style-src}, @policy.build
+ @policy.style_src_attr :self
+ assert_match %r{style-src-attr 'self'}, @policy.build
+
+ @policy.style_src_attr false
+ assert_no_match %r{style-src-attr}, @policy.build
+
+ @policy.style_src_elem :self
+ assert_match %r{style-src-elem 'self'}, @policy.build
+
+ @policy.style_src_elem false
+ assert_no_match %r{style-src-elem}, @policy.build
+
@policy.worker_src :self
assert_match %r{worker-src 'self'}, @policy.build
@@ -542,3 +566,57 @@ class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::Integration
assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"]
end
end
+
+class NonceDirectiveContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ def index
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "nonce_directive_content_security_policy_integration_test" do
+ get "/", to: "policy#index"
+ end
+ end
+
+ POLICY = ActionDispatch::ContentSecurityPolicy.new do |p|
+ p.default_src -> { :self }
+ p.script_src -> { :https }
+ p.style_src -> { :https }
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ 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.content_security_policy_nonce_directives"] = %w(script-src)
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::ContentSecurityPolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_generate_nonce_only_specified_in_nonce_directives
+ get "/"
+
+ assert_response :success
+ assert_match "script-src https: 'nonce-iyhD0Yc0W+c='", response.headers["Content-Security-Policy"]
+ assert_no_match "style-src https: 'nonce-iyhD0Yc0W+c='", response.headers["Content-Security-Policy"]
+ end
+end
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index 68817ccdea..5d12b62fd4 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -466,6 +466,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
end
test "logs exception backtrace when all lines silenced" do
+ @app = DevelopmentApp
+
output = StringIO.new
backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
backtrace_cleaner.add_silencer { true }
@@ -478,6 +480,27 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_operator((output.rewind && output.read).lines.count, :>, 10)
end
+ test "doesn't log the framework backtrace when error type is a routing error" do
+ @app = ProductionApp
+
+ output = StringIO.new
+ backtrace_cleaner = ActiveSupport::BacktraceCleaner.new
+ backtrace_cleaner.add_silencer { true }
+
+ env = { "action_dispatch.show_exceptions" => true,
+ "action_dispatch.logger" => Logger.new(output),
+ "action_dispatch.backtrace_cleaner" => backtrace_cleaner }
+
+ assert_raises ActionController::RoutingError do
+ get "/pass", headers: env
+ end
+
+ log = output.rewind && output.read
+
+ assert_includes log, "ActionController::RoutingError (No route matches [GET] \"/pass\")"
+ assert_equal 3, log.lines.count
+ end
+
test "display backtrace when error type is SyntaxError" do
@app = DevelopmentApp
@@ -521,8 +544,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@app = DevelopmentApp
Rails.stub :root, Pathname.new(".") do
cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
- bc.add_silencer { |line| line =~ /method_that_raises/ }
- bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
+ bc.add_silencer { |line| line.match?(/method_that_raises/) }
+ bc.add_silencer { |line| !line.match?(%r{test/dispatch/debug_exceptions_test.rb}) }
end
get "/framework_raises", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
@@ -573,7 +596,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@app = DevelopmentApp
Rails.stub :root, Pathname.new(".") do
cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
- bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
+ bc.add_silencer { |line| !line.match?(%r{test/dispatch/debug_exceptions_test.rb}) }
end
get "/nested_exceptions", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
@@ -608,7 +631,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@app = DevelopmentApp
Rails.stub :root, Pathname.new(".") do
cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
- bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
+ bc.add_silencer { |line| !line.match?(%r{test/dispatch/debug_exceptions_test.rb}) }
end
get "/actionable_error", headers: { "action_dispatch.backtrace_cleaner" => cleaner }
diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb
index 668469a01d..3c395ca5ee 100644
--- a/actionpack/test/dispatch/exception_wrapper_test.rb
+++ b/actionpack/test/dispatch/exception_wrapper_test.rb
@@ -21,7 +21,7 @@ module ActionDispatch
setup do
@cleaner = ActiveSupport::BacktraceCleaner.new
@cleaner.remove_filters!
- @cleaner.add_silencer { |line| line !~ /^lib/ }
+ @cleaner.add_silencer { |line| !line.match?(/^lib/) }
end
test "#source_extracts fetches source fragments for every backtrace entry" do
diff --git a/actionpack/test/dispatch/feature_policy_test.rb b/actionpack/test/dispatch/feature_policy_test.rb
new file mode 100644
index 0000000000..ebcc8a8b6d
--- /dev/null
+++ b/actionpack/test/dispatch/feature_policy_test.rb
@@ -0,0 +1,142 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+class FeaturePolicyTest < ActiveSupport::TestCase
+ def setup
+ @policy = ActionDispatch::FeaturePolicy.new
+ end
+
+ def test_mappings
+ @policy.midi :self
+ assert_equal "midi 'self'", @policy.build
+
+ @policy.midi :none
+ assert_equal "midi 'none'", @policy.build
+ end
+
+ def test_multiple_sources_for_a_single_directive
+ @policy.geolocation :self, "https://example.com"
+ assert_equal "geolocation 'self' https://example.com", @policy.build
+ end
+
+ def test_single_directive_for_multiple_directives
+ @policy.geolocation :self
+ @policy.usb :none
+ assert_equal "geolocation 'self'; usb 'none'", @policy.build
+ end
+
+ def test_multiple_directives_for_multiple_directives
+ @policy.geolocation :self, "https://example.com"
+ @policy.usb :none, "https://example.com"
+ assert_equal "geolocation 'self' https://example.com; usb 'none' https://example.com", @policy.build
+ end
+
+ def test_invalid_directive_source
+ exception = assert_raises(ArgumentError) do
+ @policy.vr [:non_existent]
+ end
+
+ assert_equal "Invalid HTTP feature policy source: [:non_existent]", exception.message
+ end
+end
+
+class FeaturePolicyIntegrationTest < ActionDispatch::IntegrationTest
+ class PolicyController < ActionController::Base
+ feature_policy only: :index do |f|
+ f.gyroscope :none
+ end
+
+ feature_policy only: :sample_controller do |f|
+ f.gyroscope nil
+ f.usb :self
+ end
+
+ feature_policy only: :multiple_directives do |f|
+ f.gyroscope nil
+ f.usb :self
+ f.autoplay "https://example.com"
+ f.payment "https://secure.example.com"
+ end
+
+ def index
+ head :ok
+ end
+
+ def sample_controller
+ head :ok
+ end
+
+ def multiple_directives
+ head :ok
+ end
+ end
+
+ ROUTES = ActionDispatch::Routing::RouteSet.new
+ ROUTES.draw do
+ scope module: "feature_policy_integration_test" do
+ get "/", to: "policy#index"
+ get "/sample_controller", to: "policy#sample_controller"
+ get "/multiple_directives", to: "policy#multiple_directives"
+ end
+ end
+
+ POLICY = ActionDispatch::FeaturePolicy.new do |p|
+ p.gyroscope :self
+ end
+
+ class PolicyConfigMiddleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ env["action_dispatch.feature_policy"] = POLICY
+ env["action_dispatch.show_exceptions"] = false
+
+ @app.call(env)
+ end
+ end
+
+ APP = build_app(ROUTES) do |middleware|
+ middleware.use PolicyConfigMiddleware
+ middleware.use ActionDispatch::FeaturePolicy::Middleware
+ end
+
+ def app
+ APP
+ end
+
+ def test_generates_feature_policy_header
+ get "/"
+ assert_policy "gyroscope 'none'"
+ end
+
+ def test_generates_per_controller_feature_policy_header
+ get "/sample_controller"
+ assert_policy "usb 'self'"
+ end
+
+ def test_generates_multiple_directives_feature_policy_header
+ get "/multiple_directives"
+ assert_policy "usb 'self'; autoplay https://example.com; payment https://secure.example.com"
+ end
+
+ private
+ def env_config
+ Rails.application.env_config
+ end
+
+ def feature_policy
+ env_config["action_dispatch.feature_policy"]
+ end
+
+ def feature_policy=(policy)
+ env_config["action_dispatch.feature_policy"] = policy
+ end
+
+ def assert_policy(expected)
+ assert_response :success
+ assert_equal expected, response.headers["Feature-Policy"]
+ end
+end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 50f6c06fee..b0b2aa0cc1 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -175,6 +175,12 @@ class MimeTypeTest < ActiveSupport::TestCase
assert Mime[:html] =~ "application/xhtml+xml"
end
+ test "match?" do
+ assert Mime[:js].match?("text/javascript")
+ assert Mime[:js].match?("application/javascript")
+ assert_not Mime[:js].match?("text/html")
+ end
+
test "can be initialized with wildcards" do
assert_equal "*/*", Mime::Type.new("*/*").to_s
assert_equal "text/*", Mime::Type.new("text/*").to_s
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index 63c147cb1b..f20043b9ac 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -304,7 +304,7 @@ module TestGenerationPrefix
assert_equal "/omg/blog/posts/1", path
end
- test "[OBJECT] generating engine's route with named helpers" do
+ test "[OBJECT] generating engine's route with named route helpers" do
path = engine_object.posts_path
assert_equal "/awesome/blog/posts", path
diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb
index 2a48a12497..bbf98912f3 100644
--- a/actionpack/test/dispatch/request/json_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb
@@ -68,7 +68,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
post "/parse", params: json, headers: { "CONTENT_TYPE" => "application/json", "action_dispatch.show_exceptions" => true, "action_dispatch.logger" => ActiveSupport::Logger.new(output) }
assert_response :bad_request
output.rewind && err = output.read
- assert err =~ /Error occurred while parsing request parameters/
+ assert err.match?(/Error occurred while parsing request parameters/)
end
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 0ec8dd25e0..806b7d0559 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -865,12 +865,28 @@ class RequestFormat < BaseRequestTest
assert_not_predicate request.format, :json?
end
- test "format does not throw exceptions when malformed parameters" do
+ test "format does not throw exceptions when malformed GET parameters" do
request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
assert request.formats
assert_predicate request.format, :html?
end
+ test "format does not throw exceptions when invalid POST parameters" do
+ body = "{record:{content:127.0.0.1}}"
+ request = stub_request(
+ "REQUEST_METHOD" => "POST",
+ "CONTENT_LENGTH" => body.length,
+ "CONTENT_TYPE" => "application/json",
+ "rack.input" => StringIO.new(body),
+ "action_dispatch.logger" => Logger.new(output = StringIO.new)
+ )
+ assert request.formats
+ assert request.format.html?
+
+ output.rewind && (err = output.read)
+ assert_match(/Error occurred while parsing request parameters/, err)
+ end
+
test "formats with xhr request" do
request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
"QUERY_STRING" => ""
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index b67b1dd347..d4a667a13a 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -12,7 +12,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
class IpRestrictor
def self.matches?(request)
- request.ip =~ /192\.168\.1\.1\d\d/
+ /192\.168\.1\.1\d\d/.match?(request.ip)
end
end
@@ -3823,7 +3823,7 @@ private
end
def method_missing(method, *args, &block)
- if method.to_s =~ /_(path|url)$/
+ if method.to_s.match?(/_(path|url)$/)
@app.routes.url_helpers.send(method, *args, &block)
else
super
@@ -5137,7 +5137,7 @@ class TestRecognizePath < ActionDispatch::IntegrationTest
end
def matches?(request)
- request.path_parameters[key] =~ pattern
+ pattern.match?(request.path_parameters[key])
end
end
@@ -5147,8 +5147,8 @@ class TestRecognizePath < ActionDispatch::IntegrationTest
get "/hash/:foo", to: "pages#show", constraints: { foo: /foo/ }
get "/hash/:bar", to: "pages#show", constraints: { bar: /bar/ }
- get "/proc/:foo", to: "pages#show", constraints: proc { |r| r.path_parameters[:foo] =~ /foo/ }
- get "/proc/:bar", to: "pages#show", constraints: proc { |r| r.path_parameters[:bar] =~ /bar/ }
+ get "/proc/:foo", to: "pages#show", constraints: proc { |r| /foo/.match?(r.path_parameters[:foo]) }
+ get "/proc/:bar", to: "pages#show", constraints: proc { |r| /bar/.match?(r.path_parameters[:bar]) }
get "/class/:foo", to: "pages#show", constraints: PageConstraint.new(:foo, /foo/)
get "/class/:bar", to: "pages#show", constraints: PageConstraint.new(:bar, /bar/)
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index baf46e7c7e..44fe433c5f 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -42,7 +42,7 @@ class RedirectSSLTest < SSLTest
end
test "exclude can avoid redirect" do
- excluding = { exclude: -> request { request.path =~ /healthcheck/ } }
+ excluding = { exclude: -> request { request.path.match?(/healthcheck/) } }
assert_not_redirected "http://example.org/healthcheck", redirect: excluding
assert_redirected from: "http://example.org/", redirect: excluding
@@ -209,7 +209,7 @@ class SecureCookiesTest < SSLTest
end
def test_cookies_as_not_secure_with_exclude
- excluding = { exclude: -> request { request.domain =~ /example/ } }
+ excluding = { exclude: -> request { /example/.match?(request.domain) } }
get headers: { "Set-Cookie" => DEFAULT }, ssl_options: { redirect: excluding }
assert_cookies(*DEFAULT.split("\n"))
diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb
index 7ef306d04b..d3b16d0328 100644
--- a/actionpack/test/dispatch/system_testing/driver_test.rb
+++ b/actionpack/test/dispatch/system_testing/driver_test.rb
@@ -120,4 +120,17 @@ class DriverTest < ActiveSupport::TestCase
driver.use
end
end
+
+ test "preloads browser's driver_path" do
+ called = false
+
+ original_driver_path = ::Selenium::WebDriver::Chrome::Service.driver_path
+ ::Selenium::WebDriver::Chrome::Service.driver_path = -> { called = true }
+
+ ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :chrome)
+
+ assert called
+ ensure
+ ::Selenium::WebDriver::Chrome::Service.driver_path = original_driver_path
+ end
end
diff --git a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
index b0b36f9d74..5d69a887ef 100644
--- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -6,60 +6,93 @@ require "capybara/dsl"
require "selenium/webdriver"
class ScreenshotHelperTest < ActiveSupport::TestCase
+ def setup
+ @new_test = DrivenBySeleniumWithChrome.new("x")
+ @new_test.send("_screenshot_counter=", nil)
+ end
+
test "image path is saved in tmp directory" do
- new_test = DrivenBySeleniumWithChrome.new("x")
+ Rails.stub :root, Pathname.getwd do
+ assert_equal Rails.root.join("tmp/screenshots/0_x.png").to_s, @new_test.send(:image_path)
+ end
+ end
+
+ test "image path unique counter is changed when incremented" do
+ @new_test.send(:increment_unique)
Rails.stub :root, Pathname.getwd do
- assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/1_x.png").to_s, @new_test.send(:image_path)
end
end
- test "image path includes failures text if test did not pass" do
- new_test = DrivenBySeleniumWithChrome.new("x")
+ # To allow multiple screenshots in same test
+ test "image path unique counter generates different path in same test" do
+ Rails.stub :root, Pathname.getwd do
+ @new_test.send(:increment_unique)
+ assert_equal Rails.root.join("tmp/screenshots/1_x.png").to_s, @new_test.send(:image_path)
+
+ @new_test.send(:increment_unique)
+ assert_equal Rails.root.join("tmp/screenshots/2_x.png").to_s, @new_test.send(:image_path)
+ end
+ end
+ test "image path includes failures text if test did not pass" do
Rails.stub :root, Pathname.getwd do
- new_test.stub :passed?, false do
- assert_equal Rails.root.join("tmp/screenshots/failures_x.png").to_s, new_test.send(:image_path)
+ @new_test.stub :passed?, false do
+ assert_equal Rails.root.join("tmp/screenshots/failures_x.png").to_s, @new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/failures_x.html").to_s, @new_test.send(:html_path)
end
end
end
test "image path does not include failures text if test skipped" do
- new_test = DrivenBySeleniumWithChrome.new("x")
-
Rails.stub :root, Pathname.getwd do
- new_test.stub :passed?, false do
- new_test.stub :skipped?, true do
- assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
+ @new_test.stub :passed?, false do
+ @new_test.stub :skipped?, true do
+ assert_equal Rails.root.join("tmp/screenshots/0_x.png").to_s, @new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/0_x.html").to_s, @new_test.send(:html_path)
end
end
end
end
- test "image name truncates names over 225 characters" do
- new_test = DrivenBySeleniumWithChrome.new("x" * 400)
+ test "image name truncates names over 225 characters including counter" do
+ long_test = DrivenBySeleniumWithChrome.new("x" * 400)
Rails.stub :root, Pathname.getwd do
- assert_equal Rails.root.join("tmp/screenshots/#{"x" * 225}.png").to_s, new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/0_#{"x" * 223}.png").to_s, long_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/0_#{"x" * 223}.html").to_s, long_test.send(:html_path)
end
end
test "defaults to simple output for the screenshot" do
- new_test = DrivenBySeleniumWithChrome.new("x")
- assert_equal "simple", new_test.send(:output_type)
+ assert_equal "simple", @new_test.send(:output_type)
+ end
+
+ test "display_image return html path when RAILS_SYSTEM_TESTING_SCREENSHOT_HTML environment" do
+ original_html_setting = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT_HTML"]
+ ENV["RAILS_SYSTEM_TESTING_SCREENSHOT_HTML"] = "1"
+
+ assert @new_test.send(:save_html?)
+
+ Rails.stub :root, Pathname.getwd do
+ @new_test.stub :passed?, false do
+ assert_match %r|\[Screenshot HTML\].+?tmp/screenshots/failures_x\.html|, @new_test.send(:display_image)
+ end
+ end
+ ensure
+ ENV["RAILS_SYSTEM_TESTING_SCREENSHOT_HTML"] = original_html_setting
end
test "display_image return artifact format when specify RAILS_SYSTEM_TESTING_SCREENSHOT environment" do
original_output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"]
ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] = "artifact"
- new_test = DrivenBySeleniumWithChrome.new("x")
-
- assert_equal "artifact", new_test.send(:output_type)
+ assert_equal "artifact", @new_test.send(:output_type)
Rails.stub :root, Pathname.getwd do
- new_test.stub :passed?, false do
- assert_match %r|url=artifact://.+?tmp/screenshots/failures_x\.png|, new_test.send(:display_image)
+ @new_test.stub :passed?, false do
+ assert_match %r|url=artifact://.+?tmp/screenshots/failures_x\.png|, @new_test.send(:display_image)
end
end
ensure
@@ -67,10 +100,8 @@ class ScreenshotHelperTest < ActiveSupport::TestCase
end
test "image path returns the absolute path from root" do
- new_test = DrivenBySeleniumWithChrome.new("x")
-
Rails.stub :root, Pathname.getwd.join("..") do
- assert_equal Rails.root.join("tmp/screenshots/x.png").to_s, new_test.send(:image_path)
+ assert_equal Rails.root.join("tmp/screenshots/0_x.png").to_s, @new_test.send(:image_path)
end
end
end
diff --git a/actionpack/test/dispatch/system_testing/system_test_case_test.rb b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
index 3319db1665..d235f7ad89 100644
--- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb
+++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
@@ -36,12 +36,10 @@ class SetDriverToSeleniumHeadlessFirefoxTest < DrivenBySeleniumWithHeadlessFiref
end
class SetHostTest < DrivenByRackTest
- test "sets default host" do
- assert_equal "http://127.0.0.1", Capybara.app_host
- end
-
test "overrides host" do
- host! "http://example.com"
+ assert_deprecated do
+ host! "http://example.com"
+ end
assert_equal "http://example.com", Capybara.app_host
end
diff --git a/actiontext/app/helpers/action_text/content_helper.rb b/actiontext/app/helpers/action_text/content_helper.rb
index ed2887d865..1e05f572f7 100644
--- a/actiontext/app/helpers/action_text/content_helper.rb
+++ b/actiontext/app/helpers/action_text/content_helper.rb
@@ -4,7 +4,7 @@ require "rails-html-sanitizer"
module ActionText
module ContentHelper
- mattr_accessor(:sanitizer) { Rails::Html::Sanitizer.white_list_sanitizer.new }
+ mattr_accessor(:sanitizer) { Rails::Html::Sanitizer.safe_list_sanitizer.new }
mattr_accessor(:allowed_tags) { sanitizer.class.allowed_tags + [ ActionText::Attachment::TAG_NAME, "figure", "figcaption" ] }
mattr_accessor(:allowed_attributes) { sanitizer.class.allowed_attributes + ActionText::Attachment::ATTRIBUTES }
mattr_accessor(:scrubber)
diff --git a/actiontext/app/helpers/action_text/tag_helper.rb b/actiontext/app/helpers/action_text/tag_helper.rb
index 1dc6202ae1..fe40be74f5 100644
--- a/actiontext/app/helpers/action_text/tag_helper.rb
+++ b/actiontext/app/helpers/action_text/tag_helper.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "active_support/core_ext/object/try"
require "action_view/helpers/tags/placeholderable"
module ActionText
diff --git a/actiontext/lib/action_text/attachables/remote_image.rb b/actiontext/lib/action_text/attachables/remote_image.rb
index 650b11862b..c5819ef1f5 100644
--- a/actiontext/lib/action_text/attachables/remote_image.rb
+++ b/actiontext/lib/action_text/attachables/remote_image.rb
@@ -14,7 +14,7 @@ module ActionText
private
def content_type_is_image?(content_type)
- content_type.to_s =~ /^image(\/.+|$)/
+ content_type.to_s.match?(/^image(\/.+|$)/)
end
def attributes_from_node(node)
diff --git a/actiontext/lib/action_text/attachment.rb b/actiontext/lib/action_text/attachment.rb
index e90a3e7d48..4bd537c7c2 100644
--- a/actiontext/lib/action_text/attachment.rb
+++ b/actiontext/lib/action_text/attachment.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/object/try"
+
module ActionText
class Attachment
include Attachments::TrixConversion, Attachments::Minification, Attachments::Caching
diff --git a/actiontext/lib/action_text/attachment_gallery.rb b/actiontext/lib/action_text/attachment_gallery.rb
index 45afbff058..48ba9f830c 100644
--- a/actiontext/lib/action_text/attachment_gallery.rb
+++ b/actiontext/lib/action_text/attachment_gallery.rb
@@ -23,7 +23,7 @@ module ActionText
Fragment.wrap(content).find_all(SELECTOR).select do |node|
node.children.all? do |child|
if child.text?
- child.text =~ /\A(\n|\ )*\z/
+ /\A(\n|\ )*\z/.match?(child.text)
else
child.matches? ATTACHMENT_SELECTOR
end
diff --git a/actiontext/lib/action_text/attachments/trix_conversion.rb b/actiontext/lib/action_text/attachments/trix_conversion.rb
index 24937d6c22..15319f4e37 100644
--- a/actiontext/lib/action_text/attachments/trix_conversion.rb
+++ b/actiontext/lib/action_text/attachments/trix_conversion.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/object/try"
+
module ActionText
module Attachments
module TrixConversion
diff --git a/actiontext/lib/action_text/engine.rb b/actiontext/lib/action_text/engine.rb
index 0c2aebfd08..0f55d460a1 100644
--- a/actiontext/lib/action_text/engine.rb
+++ b/actiontext/lib/action_text/engine.rb
@@ -25,6 +25,10 @@ module ActionText
def previewable_attachable?
representable?
end
+
+ def attachable_plain_text_representation(caption = nil)
+ "[#{caption || filename}]"
+ end
end
end
diff --git a/actiontext/package.json b/actiontext/package.json
index 594cfe16eb..ee44b75d2c 100644
--- a/actiontext/package.json
+++ b/actiontext/package.json
@@ -24,6 +24,6 @@
"@rails/activestorage": "^6.0.0-alpha"
},
"peerDependencies": {
- "trix": "^1.0.0"
+ "trix": "^1.2.0"
}
}
diff --git a/actiontext/test/dummy/config/initializers/backtrace_silencers.rb b/actiontext/test/dummy/config/initializers/backtrace_silencers.rb
index 59385cdf37..3c56b21b3c 100644
--- a/actiontext/test/dummy/config/initializers/backtrace_silencers.rb
+++ b/actiontext/test/dummy/config/initializers/backtrace_silencers.rb
@@ -1,7 +1,7 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!
diff --git a/actiontext/test/unit/attachment_test.rb b/actiontext/test/unit/attachment_test.rb
index 026078dcec..54831a0271 100644
--- a/actiontext/test/unit/attachment_test.rb
+++ b/actiontext/test/unit/attachment_test.rb
@@ -50,6 +50,11 @@ class ActionText::AttachmentTest < ActiveSupport::TestCase
assert_not_nil trix_attachment.attributes["content"]
end
+ test "converts to plain text" do
+ assert_equal "[Vroom vroom]", ActionText::Attachment.from_attachable(attachable, caption: "Vroom vroom").to_plain_text
+ assert_equal "[racecar.jpg]", ActionText::Attachment.from_attachable(attachable).to_plain_text
+ end
+
test "defaults trix partial to model partial" do
attachable = Page.create! title: "Homepage"
assert_equal "pages/page", attachable.to_trix_content_attachment_partial_path
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index f2f57e6a36..71a8b3fdcb 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,16 @@
+* ActionView::Helpers::SanitizeHelper: support rails-html-sanitizer 1.1.0.
+
+ *Juanito Fatas*
+
+* Added `phone_to` helper method to create a link from mobile numbers
+
+ *Pietro Moro*
+
+* annotated_source_code returns an empty array so TemplateErrors without a
+ template in the backtrace are surfaced properly by DebugExceptions.
+
+ *Guilherme Mansur*, *Kasper Timm Hansen*
+
* Add autoload for SyntaxErrorInTemplate so syntax errors are correctly raised by DebugExceptions.
*Guilherme Mansur*, *Gannon McGibbon*
diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee
index 736cab08db..9e11bfa7ed 100644
--- a/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee
+++ b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee
@@ -11,6 +11,7 @@ Rails.serializeElement = (element, additionalParam) ->
inputs.forEach (input) ->
return if !input.name || input.disabled
+ return if input.closest('fieldset[disabled]')
if matches(input, 'select')
toArray(input.options).forEach (option) ->
params.push(name: input.name, value: option.value) if option.selected
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 5253ef7b0c..40d5ed36a1 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -3,7 +3,6 @@
require "active_support/core_ext/module/attr_internal"
require "active_support/core_ext/module/attribute_accessors"
require "active_support/ordered_options"
-require "active_support/deprecation"
require "action_view/log_subscriber"
require "action_view/helpers"
require "action_view/context"
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 97a6da3634..cdf436ccae 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -9,10 +9,11 @@ module ActionView
class << self
# Supported options:
#
- # * <tt>name</tt> - Template name
- # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
- # * <tt>dependencies</tt> - An array of dependent views
- def digest(name:, format:, finder:, dependencies: nil)
+ # * <tt>name</tt> - Template name
+ # * <tt>format</tt> - Template format
+ # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
+ # * <tt>dependencies</tt> - An array of dependent views
+ def digest(name:, format: nil, finder:, dependencies: nil)
if dependencies.nil? || dependencies.empty?
cache_key = "#{name}.#{format}"
else
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index 59d70a1dc4..1e1d97fe75 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -3,7 +3,6 @@
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/hash/keys"
require "active_support/core_ext/object/inclusion"
-require "active_support/core_ext/object/try"
require "action_view/helpers/asset_url_helper"
require "action_view/helpers/tag_helper"
@@ -268,7 +267,7 @@ module ActionView
def preload_link_tag(source, options = {})
href = asset_path(source, skip_pipeline: options.delete(:skip_pipeline))
extname = File.extname(source).downcase.delete(".")
- mime_type = options.delete(:type) || Template::Types[extname].try(:to_s)
+ mime_type = options.delete(:type) || Template::Types[extname]&.to_s
as_type = options.delete(:as) || resolve_link_as(extname, mime_type)
crossorigin = options.delete(:crossorigin)
crossorigin = "anonymous" if crossorigin == true || (crossorigin.blank? && as_type == "font")
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
index cc62783d60..295f945325 100644
--- a/actionview/lib/action_view/helpers/asset_url_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -133,6 +133,8 @@ module ActionView
# which is implemented by sprockets-rails.
#
# asset_path("application.js") # => "/assets/application-60aa4fdc5cea14baf5400fba1abf4f2a46a5166bad4772b1effe341570f07de9.js"
+ # asset_path('application.js', host: 'example.com') # => "//example.com/assets/application.js"
+ # asset_path("application.js", host: 'example.com', protocol: 'https') # => "https://example.com/assets/application.js"
#
# === Without the asset pipeline (<tt>skip_pipeline: true</tt>)
#
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index a7747456a4..cf8f7de931 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -566,9 +566,10 @@ module ActionView
# an ActiveSupport::TimeZone.
#
# By default, +model+ is the ActiveSupport::TimeZone constant (which can
- # be obtained in Active Record as a value object). The only requirement
- # is that the +model+ parameter be an object that responds to +all+, and
- # returns an array of objects that represent time zones.
+ # be obtained in Active Record as a value object). The +model+ parameter
+ # must respond to +all+ and return an array of objects that represent time
+ # zones; each object must respond to +name+. If a Regexp is given it will
+ # attempt to match the zones using <code>match?</code> method.
#
# NOTE: Only the option tags are returned, you have to wrap this call in
# a regular HTML select tag.
@@ -580,7 +581,7 @@ module ActionView
if priority_zones
if priority_zones.is_a?(Regexp)
- priority_zones = zones.select { |z| z =~ priority_zones }
+ priority_zones = zones.select { |z| z.match?(priority_zones) }
end
zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
index f4fa133f55..d6d0635911 100644
--- a/actionview/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require "active_support/core_ext/object/try"
require "rails-html-sanitizer"
+require "active_support/deprecation"
module ActionView
# = Action View Sanitize Helpers
@@ -17,7 +17,7 @@ module ActionView
# ASCII, and hex character references to work around these protocol filters.
# All special characters will be escaped.
#
- # The default sanitizer is Rails::Html::WhiteListSanitizer. See {Rails HTML
+ # The default sanitizer is Rails::Html::SafeListSanitizer. See {Rails HTML
# Sanitizers}[https://github.com/rails/rails-html-sanitizer] for more information.
#
# Custom sanitization rules can also be provided.
@@ -80,12 +80,12 @@ module ActionView
# config.action_view.sanitized_allowed_tags = ['strong', 'em', 'a']
# config.action_view.sanitized_allowed_attributes = ['href', 'title']
def sanitize(html, options = {})
- self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe)
+ self.class.safe_list_sanitizer.sanitize(html, options)&.html_safe
end
# Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute.
def sanitize_css(style)
- self.class.white_list_sanitizer.sanitize_css(style)
+ self.class.safe_list_sanitizer.sanitize_css(style)
end
# Strips all HTML tags from +html+, including comments and special characters.
@@ -123,20 +123,14 @@ module ActionView
end
module ClassMethods #:nodoc:
- attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
-
- # Vendors the full, link and white list sanitizers.
- # Provided strictly for compatibility and can be removed in Rails 6.
- def sanitizer_vendor
- Rails::Html::Sanitizer
- end
+ attr_writer :full_sanitizer, :link_sanitizer, :safe_list_sanitizer
def sanitized_allowed_tags
- sanitizer_vendor.white_list_sanitizer.allowed_tags
+ safe_list_sanitizer.allowed_tags
end
def sanitized_allowed_attributes
- sanitizer_vendor.white_list_sanitizer.allowed_attributes
+ safe_list_sanitizer.allowed_attributes
end
# Gets the Rails::Html::FullSanitizer instance used by +strip_tags+. Replace with
@@ -145,9 +139,8 @@ module ActionView
# class Application < Rails::Application
# config.action_view.full_sanitizer = MySpecialSanitizer.new
# end
- #
def full_sanitizer
- @full_sanitizer ||= sanitizer_vendor.full_sanitizer.new
+ @full_sanitizer ||= Rails::Html::Sanitizer.full_sanitizer.new
end
# Gets the Rails::Html::LinkSanitizer instance used by +strip_links+.
@@ -156,20 +149,18 @@ module ActionView
# class Application < Rails::Application
# config.action_view.link_sanitizer = MySpecialSanitizer.new
# end
- #
def link_sanitizer
- @link_sanitizer ||= sanitizer_vendor.link_sanitizer.new
+ @link_sanitizer ||= Rails::Html::Sanitizer.link_sanitizer.new
end
- # Gets the Rails::Html::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
+ # Gets the Rails::Html::SafeListSanitizer instance used by sanitize and +sanitize_css+.
# Replace with any object that responds to +sanitize+.
#
# class Application < Rails::Application
- # config.action_view.white_list_sanitizer = MySpecialSanitizer.new
+ # config.action_view.safe_list_sanitizer = MySpecialSanitizer.new
# end
- #
- def white_list_sanitizer
- @white_list_sanitizer ||= sanitizer_vendor.white_list_sanitizer.new
+ def safe_list_sanitizer
+ @safe_list_sanitizer ||= Rails::Html::Sanitizer.safe_list_sanitizer.new
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/date_field.rb b/actionview/lib/action_view/helpers/tags/date_field.rb
index ceaabfa99c..9cdfc6991f 100644
--- a/actionview/lib/action_view/helpers/tags/date_field.rb
+++ b/actionview/lib/action_view/helpers/tags/date_field.rb
@@ -6,7 +6,7 @@ module ActionView
class DateField < DatetimeField # :nodoc:
private
def format_date(value)
- value.try(:strftime, "%Y-%m-%d")
+ value&.strftime("%Y-%m-%d")
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/datetime_local_field.rb b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb
index 8908bf9948..f0834ac6ce 100644
--- a/actionview/lib/action_view/helpers/tags/datetime_local_field.rb
+++ b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb
@@ -12,7 +12,7 @@ module ActionView
private
def format_date(value)
- value.try(:strftime, "%Y-%m-%dT%T")
+ value&.strftime("%Y-%m-%dT%T")
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/month_field.rb b/actionview/lib/action_view/helpers/tags/month_field.rb
index 463866a181..b582bb4f79 100644
--- a/actionview/lib/action_view/helpers/tags/month_field.rb
+++ b/actionview/lib/action_view/helpers/tags/month_field.rb
@@ -6,7 +6,7 @@ module ActionView
class MonthField < DatetimeField # :nodoc:
private
def format_date(value)
- value.try(:strftime, "%Y-%m")
+ value&.strftime("%Y-%m")
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/time_field.rb b/actionview/lib/action_view/helpers/tags/time_field.rb
index e74c578db9..e5e0b84891 100644
--- a/actionview/lib/action_view/helpers/tags/time_field.rb
+++ b/actionview/lib/action_view/helpers/tags/time_field.rb
@@ -6,7 +6,7 @@ module ActionView
class TimeField < DatetimeField # :nodoc:
private
def format_date(value)
- value.try(:strftime, "%T.%L")
+ value&.strftime("%T.%L")
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/week_field.rb b/actionview/lib/action_view/helpers/tags/week_field.rb
index 5a403ed91d..7828a3149f 100644
--- a/actionview/lib/action_view/helpers/tags/week_field.rb
+++ b/actionview/lib/action_view/helpers/tags/week_field.rb
@@ -6,7 +6,7 @@ module ActionView
class WeekField < DatetimeField # :nodoc:
private
def format_date(value)
- value.try(:strftime, "%Y-W%V")
+ value&.strftime("%Y-W%V")
end
end
end
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
index 8203a43239..980a89a7b6 100644
--- a/actionview/lib/action_view/helpers/text_helper.rb
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -228,7 +228,7 @@ module ActionView
# pluralize(2, 'Person', locale: :de)
# # => 2 Personen
def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale)
- word = if count == 1 || count.to_s =~ /^1(\.0+)?$/
+ word = if count == 1 || count.to_s.match?(/^1(\.0+)?$/)
singular
else
plural || singular.pluralize(locale)
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 4b3a258287..61ab3c2e13 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -45,7 +45,7 @@ module ActionView
def _back_url # :nodoc:
_filtered_referrer || "javascript:history.back()"
end
- protected :_back_url
+ private :_back_url
def _filtered_referrer # :nodoc:
if controller.respond_to?(:request)
@@ -56,12 +56,12 @@ module ActionView
end
rescue URI::InvalidURIError
end
- protected :_filtered_referrer
+ private :_filtered_referrer
# Creates an anchor element of the given +name+ using a URL created by the set of +options+.
# See the valid options in the documentation for +url_for+. It's also possible to
- # pass a String instead of an options hash, which generates an anchor element that uses the
- # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead
+ # pass a \String instead of an options hash, which generates an anchor element that uses the
+ # value of the \String as the href for the link. Using a <tt>:back</tt> \Symbol instead
# of an options hash will generate a link to the referrer (a JavaScript back link
# will be used in place of a referrer if none exists). If +nil+ is passed as the name
# the value of the link itself will become the name.
@@ -226,7 +226,7 @@ module ActionView
# The +options+ hash accepts the same options as +url_for+.
#
# There are a few special +html_options+:
- # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
+ # * <tt>:method</tt> - \Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
# <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>.
# * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
# * <tt>:data</tt> - This option can be used to add custom data attributes.
@@ -235,7 +235,7 @@ module ActionView
# * <tt>:form</tt> - This hash will be form attributes
# * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
# be placed
- # * <tt>:params</tt> - Hash of parameters to be rendered as hidden fields within the form.
+ # * <tt>:params</tt> - \Hash of parameters to be rendered as hidden fields within the form.
#
# ==== Data attributes
#
@@ -571,6 +571,101 @@ module ActionView
end
end
+ # Creates an SMS anchor link tag to the specified +phone_number+, which is
+ # also used as the name of the link unless +name+ is specified. Additional
+ # HTML attributes for the link can be passed in +html_options+.
+ #
+ # When clicked, an SMS message is prepopulated with the passed phone number
+ # and optional +body+ value.
+ #
+ # +sms_to+ has a +body+ option for customizing the SMS message itself by
+ # passing special keys to +html_options+.
+ #
+ # ==== Options
+ # * <tt>:body</tt> - Preset the body of the message.
+ #
+ # ==== Examples
+ # sms_to "5155555785"
+ # # => <a href="sms:5155555785;">5155555785</a>
+ #
+ # sms_to "5155555785", "Text me"
+ # # => <a href="sms:5155555785;">Text me</a>
+ #
+ # sms_to "5155555785", "Text me",
+ # body: "Hello Jim I have a question about your product."
+ # # => <a href="sms:5155555785;?body=Hello%20Jim%20I%20have%20a%20question%20about%20your%20product">Text me</a>
+ #
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
+ #
+ # <%= sms_to "5155555785" do %>
+ # <strong>Text me:</strong>
+ # <% end %>
+ # # => <a href="sms:5155555785;">
+ # <strong>Text me:</strong>
+ # </a>
+ def sms_to(phone_number, name = nil, html_options = {}, &block)
+ html_options, name = name, nil if block_given?
+ html_options = (html_options || {}).stringify_keys
+
+ extras = %w{ body }.map! { |item|
+ option = html_options.delete(item).presence || next
+ "#{item.dasherize}=#{ERB::Util.url_encode(option)}"
+ }.compact
+ extras = extras.empty? ? "" : "?&" + extras.join("&")
+
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
+ html_options["href"] = "sms:#{encoded_phone_number};#{extras}"
+
+ content_tag("a", name || phone_number, html_options, &block)
+ end
+
+ # Creates a TEL anchor link tag to the specified +phone_number+, which is
+ # also used as the name of the link unless +name+ is specified. Additional
+ # HTML attributes for the link can be passed in +html_options+.
+ #
+ # When clicked, the default app to make calls is opened, and it
+ # is prepopulated with the passed phone number and optional
+ # +country_code+ value.
+ #
+ # +phone_to+ has an optional +country_code+ option which automatically adds the country
+ # code as well as the + sign in the phone numer that gets prepopulated,
+ # for example if +country_code: "01"+ +\+01+ will be prepended to the
+ # phone numer, by passing special keys to +html_options+.
+ #
+ # ==== Options
+ # * <tt>:country_code</tt> - Prepends the country code to the number
+ #
+ # ==== Examples
+ # phone_to "1234567890"
+ # # => <a href="tel:1234567890">1234567890</a>
+ #
+ # phone_to "1234567890", "Phone me"
+ # # => <a href="tel:134567890">Phone me</a>
+ #
+ # phone_to "1234567890", "Phone me", country_code: "01"
+ # # => <a href="tel:+015155555785">Phone me</a>
+ #
+ # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example:
+ #
+ # <%= phone_to "1234567890" do %>
+ # <strong>Phone me:</strong>
+ # <% end %>
+ # # => <a href="tel:1234567890">
+ # <strong>Phone me:</strong>
+ # </a>
+ def phone_to(phone_number, name = nil, html_options = {}, &block)
+ html_options, name = name, nil if block_given?
+ html_options = (html_options || {}).stringify_keys
+
+ country_code = html_options.delete("country_code").presence
+ country_code = country_code.nil? ? "" : "+#{ERB::Util.url_encode(country_code)}"
+
+ encoded_phone_number = ERB::Util.url_encode(phone_number)
+ html_options["href"] = "tel:#{country_code}#{encoded_phone_number}"
+
+ content_tag("a", name || phone_number, html_options, &block)
+ end
+
private
def convert_options_to_data_attributes(options, html_options)
if html_options
@@ -594,7 +689,7 @@ module ActionView
end
def add_method_to_attributes!(html_options, method)
- if method_not_get_method?(method) && html_options["rel"] !~ /nofollow/
+ if method_not_get_method?(method) && !html_options["rel"]&.match?(/nofollow/)
if html_options["rel"].blank?
html_options["rel"] = "nofollow"
else
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
index be21ff0e5d..b21dc1b9b3 100644
--- a/actionview/lib/action_view/layouts.rb
+++ b/actionview/lib/action_view/layouts.rb
@@ -306,7 +306,7 @@ module ActionView
RUBY
when Proc
define_method :_layout_from_proc, &_layout
- protected :_layout_from_proc
+ private :_layout_from_proc
<<-RUBY
result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'})
return #{default_behavior} if result.nil?
@@ -395,7 +395,7 @@ module ActionView
end
def _normalize_layout(value)
- value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value
+ value.is_a?(String) && !value.match?(/\blayouts/) ? "layouts/#{value}" : value
end
# Returns the default layout for this controller.
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 138ffa515b..f050d54e27 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -3,7 +3,6 @@
require "concurrent/map"
require "active_support/core_ext/module/remove_method"
require "active_support/core_ext/module/attribute_accessors"
-require "active_support/deprecation"
require "action_view/template/resolver"
module ActionView
@@ -30,7 +29,7 @@ module ActionView
Accessors.define_method(:"default_#{name}", &block)
Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{name}
- @details.fetch(:#{name}, [])
+ @details[:#{name}] || []
end
def #{name}=(value)
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 08a280c7ee..5942717b8d 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -62,7 +62,7 @@ module ActionView
output = ActionView::StreamingBuffer.new(buffer)
yielder = lambda { |*name| view._layout_for(*name) }
- instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
+ instrument(:template, identifier: template.identifier, layout: (layout && layout.virtual_path)) do
outer_config = I18n.config
fiber = Fiber.new do
I18n.config = outer_config
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index b2d7332572..da0b2dc0d2 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/object/try"
-
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
def render(context, options)
@@ -54,7 +52,7 @@ module ActionView
# supplied as well.
def render_template(view, template, layout_name, locals)
render_with_layout(view, template, layout_name, locals) do |layout|
- instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
+ instrument(:template, identifier: template.identifier, layout: (layout && layout.virtual_path)) do
template.render(view, locals) { |*name| view._layout_for(*name) }
end
end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 88320dcd0d..3877108fef 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-require "active_support/core_ext/object/try"
require "active_support/core_ext/kernel/singleton_class"
-require "active_support/deprecation"
require "thread"
require "delegate"
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index feceef15f9..7fc74a5502 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -81,8 +81,8 @@ module ActionView
end
end
- def source_extract(indentation = 0, output = :console)
- return unless num = line_number
+ def source_extract(indentation = 0)
+ return [] unless num = line_number
num = num.to_i
source_code = @template.source.split("\n")
@@ -91,9 +91,9 @@ module ActionView
end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min
indent = end_on_line.to_s.size + indentation
- return unless source_code = source_code[start_on_line..end_on_line]
+ return [] unless source_code = source_code[start_on_line..end_on_line]
- formatted_code_for(source_code, start_on_line, indent, output)
+ formatted_code_for(source_code, start_on_line, indent)
end
def sub_template_of(template_path)
@@ -122,15 +122,11 @@ module ActionView
end + file_name
end
- def formatted_code_for(source_code, line_counter, indent, output)
- start_value = (output == :html) ? {} : []
- source_code.inject(start_value) do |result, line|
+ def formatted_code_for(source_code, line_counter, indent)
+ indent_template = "%#{indent}s: %s"
+ source_code.map do |line|
line_counter += 1
- if output == :html
- result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line])
- else
- result << "%#{indent}s: %s" % [line_counter, line]
- end
+ indent_template % [line_counter, line]
end
end
end
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index 6450513003..c7ef456125 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/deprecation"
-
module ActionView #:nodoc:
# = Action View Template Handlers
class Template #:nodoc:
diff --git a/actionview/lib/action_view/template/html.rb b/actionview/lib/action_view/template/html.rb
index ecd1c31e79..563bffd333 100644
--- a/actionview/lib/action_view/template/html.rb
+++ b/actionview/lib/action_view/template/html.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/deprecation"
-
module ActionView #:nodoc:
# = Action View HTML Template
class Template #:nodoc:
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index 539bedcdf0..03eac29bb4 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -7,10 +7,15 @@ module ActionView #:nodoc:
# file system. This is used internally by Rails' own test suite, and is
# useful for testing extensions that have no way of knowing what the file
# system will look like at runtime.
- class FixtureResolver < PathResolver
+ class FixtureResolver < OptimizedFileSystemResolver
def initialize(hash = {}, pattern = nil)
- super(pattern)
+ super("")
+ if pattern
+ ActiveSupport::Deprecation.warn "Specifying a custom path for #{self.class} is deprecated. Implement a custom Resolver subclass instead."
+ @pattern = pattern
+ end
@hash = hash
+ @path = ""
end
def data
@@ -23,25 +28,32 @@ module ActionView #:nodoc:
private
def query(path, exts, _, locals, cache:)
- query = +""
- EXTENSIONS.each do |ext, prefix|
- query << "(" << exts[ext].map { |e| e && Regexp.escape("#{prefix}#{e}") }.join("|") << "|)"
- end
- query = /^(#{Regexp.escape(path)})#{query}$/
+ regex = build_regex(path, exts)
- templates = []
- @hash.each do |_path, source|
- next unless query.match?(_path)
+ @hash.select do |_path, _|
+ ("/" + _path).match?(regex)
+ end.map do |_path, source|
handler, format, variant = extract_handler_and_format_and_variant(_path)
- templates << Template.new(source, _path, handler,
+
+ Template.new(source, _path, handler,
virtual_path: path.virtual,
format: format,
variant: variant,
locals: locals
)
+ end.sort_by do |t|
+ match = ("/" + t.identifier).match(regex)
+ EXTENSIONS.keys.reverse.map do |ext|
+ if ext == :variants && exts[ext] == :any
+ match[ext].nil? ? 0 : 1
+ elsif match[ext].nil?
+ exts[ext].length
+ else
+ found = match[ext].to_sym
+ exts[ext].index(found)
+ end
+ end
end
-
- templates.sort_by { |t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size }
end
end
diff --git a/actionview/lib/action_view/unbound_template.rb b/actionview/lib/action_view/unbound_template.rb
index d28bab91b6..3d4434b2e9 100644
--- a/actionview/lib/action_view/unbound_template.rb
+++ b/actionview/lib/action_view/unbound_template.rb
@@ -4,9 +4,9 @@ require "concurrent/map"
module ActionView
class UnboundTemplate
- def initialize(source, identifer, handler, options)
+ def initialize(source, identifier, handler, options)
@source = source
- @identifer = identifer
+ @identifier = identifier
@handler = handler
@options = options
@@ -22,7 +22,7 @@ module ActionView
options = @options.merge(locals: locals)
Template.new(
@source,
- @identifer,
+ @identifier,
@handler,
options
)
diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb
index 6fe83dcb9a..26f3bfbcbc 100644
--- a/actionview/test/activerecord/relation_cache_test.rb
+++ b/actionview/test/activerecord/relation_cache_test.rb
@@ -17,9 +17,24 @@ class RelationCacheTest < ActionView::TestCase
end
def test_cache_relation_other
- cache(Project.all) { concat("Hello World") }
+ assert_queries(1) do
+ cache(Project.all) { concat("Hello World") }
+ end
assert_equal "Hello World", controller.cache_store.read("views/test/hello_world:fa9482a68ce25bf7589b8eddad72f736/projects-#{Project.count}")
end
def view_cache_dependencies; []; end
+
+ def assert_queries(num)
+ ActiveRecord::Base.connection.materialize_transactions
+ count = 0
+
+ ActiveSupport::Notifications.subscribe("sql.active_record") do |_name, _start, _finish, _id, payload|
+ count += 1 unless ["SCHEMA", "TRANSACTION"].include? payload[:name]
+ end
+
+ result = yield
+ assert_equal num, count, "#{count} instead of #{num} queries were executed."
+ result
+ end
end
diff --git a/actionview/test/template/date_helper_i18n_test.rb b/actionview/test/template/date_helper_i18n_test.rb
index 60303b4c91..f100a011a8 100644
--- a/actionview/test/template/date_helper_i18n_test.rb
+++ b/actionview/test/template/date_helper_i18n_test.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "abstract_unit"
+require "active_support/core_ext/integer/time"
class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase
include ActionView::Helpers::DateHelper
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
index 0a294ec674..ca904a7b1e 100644
--- a/actionview/test/template/date_helper_test.rb
+++ b/actionview/test/template/date_helper_test.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "abstract_unit"
+require "active_support/core_ext/integer/time"
class DateHelperTest < ActionView::TestCase
tests ActionView::Helpers::DateHelper
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
index d7a7b95ab3..273646b077 100644
--- a/actionview/test/template/form_options_helper_test.rb
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -47,6 +47,7 @@ class FormOptionsHelperTest < ActionView::TestCase
FakeZone = Struct.new(:name) do
def to_s; name; end
def =~(_re); end
+ def match?(_re); end
end
module ClassMethods
@@ -1266,6 +1267,7 @@ class FormOptionsHelperTest < ActionView::TestCase
@fake_timezones.each do |tz|
def tz.=~(re); %(A D).include?(name) end
+ def tz.match?(re); %(A D).include?(name) end
end
html = time_zone_select("firm", "time_zone", /A|D/)
diff --git a/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb
index faeeded1c8..b2c6ae99ed 100644
--- a/actionview/test/template/output_safety_helper_test.rb
+++ b/actionview/test/template/output_safety_helper_test.rb
@@ -43,7 +43,9 @@ class OutputSafetyHelperTest < ActionView::TestCase
joined = safe_join(["a", "b"])
assert_equal "ab", joined
- $, = "|"
+ silence_warnings do
+ $, = "|"
+ end
joined = safe_join(["a", "b"])
assert_equal "a|b", joined
ensure
@@ -108,7 +110,9 @@ class OutputSafetyHelperTest < ActionView::TestCase
test "to_sentence is not affected by $," do
separator_was = $,
- $, = "|"
+ silence_warnings do
+ $, = "|"
+ end
begin
assert_equal "one and two", to_sentence(["one", "two"])
assert_equal "one, two, and three", to_sentence(["one", "two", "three"])
diff --git a/actionview/test/template/template_error_test.rb b/actionview/test/template/template_error_test.rb
index c4dc88e4aa..643c29602b 100644
--- a/actionview/test/template/template_error_test.rb
+++ b/actionview/test/template/template_error_test.rb
@@ -34,4 +34,20 @@ class TemplateErrorTest < ActiveSupport::TestCase
assert_equal "#<ActionView::Template::Error: original>", error.inspect
end
+
+ def test_annotated_source_code_returns_empty_array_if_source_cant_be_found
+ template = Class.new do
+ def identifier
+ "something"
+ end
+ end.new
+
+ error = begin
+ raise
+ rescue
+ raise ActionView::Template::Error.new(template) rescue $!
+ end
+
+ assert_equal [], error.annotated_source_code
+ end
end
diff --git a/actionview/test/template/testing/fixture_resolver_test.rb b/actionview/test/template/testing/fixture_resolver_test.rb
index 6d0317e0c9..a6066e94c6 100644
--- a/actionview/test/template/testing/fixture_resolver_test.rb
+++ b/actionview/test/template/testing/fixture_resolver_test.rb
@@ -27,4 +27,26 @@ class FixtureResolverTest < ActiveSupport::TestCase
assert_equal :html, templates.first.format
assert_equal "variant", templates.first.variant
end
+
+ def test_should_match_locales
+ resolver = ActionView::FixtureResolver.new("arbitrary/path.erb" => "this text", "arbitrary/path.fr.erb" => "ce texte")
+ en = resolver.find_all("path", "arbitrary", false, locale: [:en], formats: [:html], variants: [], handlers: [:erb])
+ fr = resolver.find_all("path", "arbitrary", false, locale: [:fr], formats: [:html], variants: [], handlers: [:erb])
+
+ assert_equal 1, en.size
+ assert_equal 2, fr.size
+
+ assert_equal "this text", en[0].source
+ assert_equal "ce texte", fr[0].source
+ assert_equal "this text", fr[1].source
+ end
+
+ def test_should_return_all_variants_for_any
+ resolver = ActionView::FixtureResolver.new("arbitrary/path.html.erb" => "this html", "arbitrary/path.html+varient.erb" => "this text")
+ templates = resolver.find_all("path", "arbitrary", false, locale: [], formats: [:html], variants: [], handlers: [:erb])
+ assert_equal 1, templates.size, "expected one template"
+ assert_equal "this html", templates.first.source
+ templates = resolver.find_all("path", "arbitrary", false, locale: [], formats: [:html], variants: :any, handlers: [:erb])
+ assert_equal 2, templates.size, "expected all templates"
+ end
end
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 632b32f09f..ba5ae837f1 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -657,7 +657,7 @@ class UrlHelperTest < ActiveSupport::TestCase
)
end
- def test_mail_with_options
+ def test_mail_to_with_options
assert_dom_equal(
%{<a href="mailto:me@example.com?cc=ccaddress%40example.com&amp;bcc=bccaddress%40example.com&amp;body=This%20is%20the%20body%20of%20the%20message.&amp;subject=This%20is%20an%20example%20email&amp;reply-to=foo%40bar.com">My email</a>},
mail_to("me@example.com", "My email", cc: "ccaddress@example.com", bcc: "bccaddress@example.com", subject: "This is an example email", body: "This is the body of the message.", reply_to: "foo@bar.com")
@@ -708,6 +708,132 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_equal({ class: "special" }, options)
end
+ def test_sms_to
+ assert_dom_equal %{<a href="sms:15155555785;">15155555785</a>}, sms_to("15155555785")
+ assert_dom_equal %{<a href="sms:15155555785;">Jim Jones</a>}, sms_to("15155555785", "Jim Jones")
+ assert_dom_equal(
+ %{<a class="admin" href="sms:15155555785;">Jim Jones</a>},
+ sms_to("15155555785", "Jim Jones", "class" => "admin")
+ )
+ assert_equal sms_to("15155555785", "Jim Jones", "class" => "admin"),
+ sms_to("15155555785", "Jim Jones", class: "admin")
+ end
+
+ def test_sms_to_with_options
+ assert_dom_equal(
+ %{<a class="simple-class" href="sms:15155555785;?&body=Hello%20from%20Jim">Text me</a>},
+ sms_to("15155555785", "Text me", class: "simple-class", body: "Hello from Jim")
+ )
+
+ assert_dom_equal(
+ %{<a href="sms:15155555785;?&body=This%20is%20the%20body%20of%20the%20message.">Text me</a>},
+ sms_to("15155555785", "Text me", body: "This is the body of the message.")
+ )
+ end
+
+ def test_sms_to_with_img
+ assert_dom_equal %{<a href="sms:15155555785;"><img src="/feedback.png" /></a>},
+ sms_to("15155555785", raw('<img src="/feedback.png" />'))
+ end
+
+ def test_sms_to_with_html_safe_string
+ assert_dom_equal(
+ %{<a href="sms:1%2B5155555785;">1+5155555785</a>},
+ sms_to(raw("1+5155555785"))
+ )
+ end
+
+ def test_sms_to_with_nil
+ assert_dom_equal(
+ %{<a href="sms:;"></a>},
+ sms_to(nil)
+ )
+ end
+
+ def test_sms_to_returns_html_safe_string
+ assert_predicate sms_to("15155555785"), :html_safe?
+ end
+
+ def test_sms_to_with_block
+ assert_dom_equal %{<a href="sms:15155555785;"><span>Text me</span></a>},
+ sms_to("15155555785") { content_tag(:span, "Text me") }
+ end
+
+ def test_sms_to_with_block_and_options
+ assert_dom_equal %{<a class="special" href="sms:15155555785;?&body=Hello%20from%20Jim"><span>Text me</span></a>},
+ sms_to("15155555785", body: "Hello from Jim", class: "special") { content_tag(:span, "Text me") }
+ end
+
+ def test_sms_to_does_not_modify_html_options_hash
+ options = { class: "special" }
+ sms_to "15155555785", "ME!", options
+ assert_equal({ class: "special" }, options)
+ end
+
+ def test_phone_to
+ assert_dom_equal %{<a href="tel:1234567890">1234567890</a>},
+ phone_to("1234567890")
+ assert_dom_equal %{<a href="tel:1234567890">Bob</a>},
+ phone_to("1234567890", "Bob")
+ assert_dom_equal(
+ %{<a class="phoner" href="tel:1234567890">Bob</a>},
+ phone_to("1234567890", "Bob", "class" => "phoner")
+ )
+ assert_equal phone_to("1234567890", "Bob", "class" => "admin"),
+ phone_to("1234567890", "Bob", class: "admin")
+ end
+
+ def test_phone_to_with_options
+ assert_dom_equal(
+ %{<a class="example-class" href="tel:+011234567890">Phone</a>},
+ phone_to("1234567890", "Phone", class: "example-class", country_code: "01")
+ )
+
+ assert_dom_equal(
+ %{<a href="tel:+011234567890">Phone</a>},
+ phone_to("1234567890", "Phone", country_code: "01")
+ )
+ end
+
+ def test_phone_to_with_img
+ assert_dom_equal %{<a href="tel:1234567890"><img src="/feedback.png" /></a>},
+ phone_to("1234567890", raw('<img src="/feedback.png" />'))
+ end
+
+ def test_phone_to_with_html_safe_string
+ assert_dom_equal(
+ %{<a href="tel:1%2B234567890">1+234567890</a>},
+ phone_to(raw("1+234567890"))
+ )
+ end
+
+ def test_phone_to_with_nil
+ assert_dom_equal(
+ %{<a href="tel:"></a>},
+ phone_to(nil)
+ )
+ end
+
+ def test_phone_to_returns_html_safe_string
+ assert_predicate phone_to("1234567890"), :html_safe?
+ end
+
+ def test_phone_to_with_block
+ assert_dom_equal %{<a href="tel:1234567890"><span>Phone</span></a>},
+ phone_to("1234567890") { content_tag(:span, "Phone") }
+ end
+
+ def test_phone_to_with_block_and_options
+ assert_dom_equal %{<a class="special" href="tel:+011234567890"><span>Phone</span></a>},
+ phone_to("1234567890", country_code: "01", class: "special") { content_tag(:span, "Phone") }
+ end
+
+ def test_phone_to_does_not_modify_html_options_hash
+ options = { class: "special" }
+ phone_to "1234567890", "ME!", options
+ assert_equal({ class: "special" }, options)
+ end
+
def protect_against_forgery?
request_forgery
end
diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js
index 16ea114f3b..f95a57f795 100644
--- a/actionview/test/ujs/public/test/data-remote.js
+++ b/actionview/test/ujs/public/test/data-remote.js
@@ -490,4 +490,22 @@ asyncTest('changing a select option without "data-url" attribute still fires aja
setTimeout(function() { start() }, 20)
})
+asyncTest('inputs inside disabled fieldset are not submitted on remote forms', 3, function() {
+ $('form')
+ .append('<fieldset>\
+ <input name="description" value="A wise man" />\
+ </fieldset>')
+ .append('<fieldset disabled="disabled">\
+ <input name="age" />\
+ </fieldset>')
+ .bindNative('ajax:success', function(e, data, status, xhr) {
+ equal(data.params.user_name, 'john')
+ equal(data.params.description, 'A wise man')
+ equal(data.params.age, undefined)
+
+ start()
+ })
+ .triggerNative('submit')
+})
+
})()
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index 2f0d72ccb9..542a4eb2db 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,3 +1,6 @@
+* `assert_enqueued_with` and `assert_performed_with` can now test jobs with relative delay.
+
+ *Vlado Cingel*
Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/activejob/CHANGELOG.md) for previous changes.
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index 283125698d..5b10858d80 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -100,7 +100,7 @@ module ActiveJob
"executions" => executions,
"exception_executions" => exception_executions,
"locale" => I18n.locale.to_s,
- "timezone" => Time.zone.try(:name),
+ "timezone" => Time.zone&.name,
"enqueued_at" => Time.now.utc.iso8601
}
end
@@ -140,7 +140,7 @@ module ActiveJob
self.executions = job_data["executions"]
self.exception_executions = job_data["exception_executions"]
self.locale = job_data["locale"] || I18n.locale.to_s
- self.timezone = job_data["timezone"] || Time.zone.try(:name)
+ self.timezone = job_data["timezone"] || Time.zone&.name
self.enqueued_at = job_data["enqueued_at"]
end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index 463020a332..5b1af7da22 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -630,7 +630,7 @@ module ActiveJob
def prepare_args_for_assertion(args)
args.dup.tap do |arguments|
- arguments[:at] = arguments[:at].to_f if arguments[:at]
+ arguments[:at] = round_time_arguments(arguments[:at]) if arguments[:at]
arguments[:args] = round_time_arguments(arguments[:args]) if arguments[:args]
end
end
@@ -650,6 +650,7 @@ module ActiveJob
def deserialize_args_for_assertion(job)
job.dup.tap do |new_job|
+ new_job[:at] = round_time_arguments(Time.at(new_job[:at])) if new_job[:at]
new_job[:args] = ActiveJob::Arguments.deserialize(new_job[:args]) if new_job[:args]
end
end
diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb
index 32471cfacc..34dfd8eb56 100644
--- a/activejob/test/cases/test_helper_test.rb
+++ b/activejob/test/cases/test_helper_test.rb
@@ -621,6 +621,12 @@ class EnqueuedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_enqueued_with_with_relative_at_option
+ assert_enqueued_with(job: HelloJob, at: 5.minutes.from_now) do
+ HelloJob.set(wait: 5.minutes).perform_later
+ end
+ end
+
def test_assert_enqueued_with_with_no_block_with_at_option
HelloJob.set(wait_until: Date.tomorrow.noon).perform_later
assert_enqueued_with(job: HelloJob, at: Date.tomorrow.noon)
@@ -1663,6 +1669,18 @@ class PerformedJobsTest < ActiveJob::TestCase
end
end
+ def test_assert_performed_with_with_relative_at_option
+ assert_performed_with(job: HelloJob, at: 5.minutes.from_now) do
+ HelloJob.set(wait: 5.minutes).perform_later
+ end
+
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: HelloJob, at: 2.minutes.from_now) do
+ HelloJob.set(wait: 1.minute).perform_later
+ end
+ end
+ end
+
def test_assert_performed_with_without_block_with_at_option
HelloJob.set(wait_until: Date.tomorrow.noon).perform_later
diff --git a/activejob/test/jobs/timezone_dependent_job.rb b/activejob/test/jobs/timezone_dependent_job.rb
index f63ff95b95..358c3aa859 100644
--- a/activejob/test/jobs/timezone_dependent_job.rb
+++ b/activejob/test/jobs/timezone_dependent_job.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require_relative "../support/job_buffer"
+require "active_support/time"
class TimezoneDependentJob < ActiveJob::Base
def perform(now)
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
index b56dd3e591..65229ba397 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -21,7 +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",
+ "timezone" => Time.zone&.name || "UTC",
"executed_at" => Time.now.to_r
})
end
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 9d77564c61..c301f4766d 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,9 @@
+* Add *_previously_was attribute methods when dirty tracking. Example:
+ pirate.update(catchphrase: "Ahoy!")
+ pirate.previous_changes["catchphrase"] # => ["Thar She Blows!", "Ahoy!"]
+ pirate.catchphrase_previously_was # => "Thar She Blows!"
+
+ *DHH*
Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index c9140dc582..756473e38d 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -53,6 +53,7 @@ module ActiveModel
eager_autoload do
autoload :Errors
+ autoload :Error
autoload :RangeError, "active_model/errors"
autoload :StrictValidationFailed, "active_model/errors"
autoload :UnknownAttributeError, "active_model/errors"
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index 415f1f679b..1a4e0b8e59 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -352,11 +352,7 @@ module ActiveModel
def attribute_method_matchers_matching(method_name)
attribute_method_matchers_cache.compute_if_absent(method_name) do
- # Bump plain matcher to last place so that only methods that do not
- # match any other pattern match the actual attribute name.
- # This is currently only needed to support legacy usage.
- matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
- matchers.map { |matcher| matcher.match(method_name) }.compact
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
end
end
@@ -406,10 +402,6 @@ module ActiveModel
def method_name(attr_name)
@method_name % attr_name
end
-
- def plain?
- prefix.empty? && suffix.empty?
- end
end
end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 35a587658c..245616502d 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -84,6 +84,7 @@ module ActiveModel
# person.previous_changes # => {"name" => [nil, "Bill"]}
# person.name_previously_changed? # => true
# person.name_previous_change # => [nil, "Bill"]
+ # person.name_previously_was # => nil
# person.reload!
# person.previous_changes # => {}
#
@@ -122,7 +123,7 @@ module ActiveModel
included do
attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
- attribute_method_suffix "_previously_changed?", "_previous_change"
+ attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
attribute_method_affix prefix: "restore_", suffix: "!"
end
@@ -136,7 +137,7 @@ module ActiveModel
@mutations_from_database = nil
end
- # Clears dirty data and moves +changes+ to +previously_changed+ and
+ # Clears dirty data and moves +changes+ to +previous_changes+ and
# +mutations_from_database+ to +mutations_before_last_save+ respectively.
def changes_applied
unless defined?(@attributes)
@@ -180,6 +181,11 @@ module ActiveModel
mutations_before_last_save.changed?(attr_name.to_s)
end
+ # Dispatch target for <tt>*_previously_was</tt> attribute methods.
+ def attribute_previously_was(attr_name) # :nodoc:
+ mutations_before_last_save.original_value(attr_name.to_s)
+ end
+
# Restore all previous data of the provided attributes.
def restore_attributes(attr_names = changed)
attr_names.each { |attr_name| restore_attribute!(attr_name) }
diff --git a/activemodel/lib/active_model/error.rb b/activemodel/lib/active_model/error.rb
index f7267fc7bf..6deab3578d 100644
--- a/activemodel/lib/active_model/error.rb
+++ b/activemodel/lib/active_model/error.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/class/attribute"
+
module ActiveModel
# == Active \Model \Error
#
@@ -8,6 +10,89 @@ module ActiveModel
CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
MESSAGE_OPTIONS = [:message]
+ class_attribute :i18n_customize_full_message, default: false
+
+ def self.full_message(attribute, message, base_class) # :nodoc:
+ return message if attribute == :base
+ attribute = attribute.to_s
+
+ if i18n_customize_full_message && base_class.respond_to?(:i18n_scope)
+ attribute = attribute.remove(/\[\d\]/)
+ parts = attribute.split(".")
+ attribute_name = parts.pop
+ namespace = parts.join("/") unless parts.empty?
+ attributes_scope = "#{base_class.i18n_scope}.errors.models"
+
+ if namespace
+ defaults = base_class.lookup_ancestors.map do |klass|
+ [
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
+ ]
+ end
+ else
+ defaults = base_class.lookup_ancestors.map do |klass|
+ [
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
+ :"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
+ ]
+ end
+ end
+
+ defaults.flatten!
+ else
+ defaults = []
+ end
+
+ defaults << :"errors.format"
+ defaults << "%{attribute} %{message}"
+
+ attr_name = attribute.tr(".", "_").humanize
+ attr_name = base_class.human_attribute_name(attribute, default: attr_name)
+
+ I18n.t(defaults.shift,
+ default: defaults,
+ attribute: attr_name,
+ message: message)
+ end
+
+ def self.generate_message(attribute, type, base, options) # :nodoc:
+ type = options.delete(:message) if options[:message].is_a?(Symbol)
+ value = (attribute != :base ? base.send(:read_attribute_for_validation, attribute) : nil)
+
+ options = {
+ model: base.model_name.human,
+ attribute: base.class.human_attribute_name(attribute),
+ value: value,
+ object: base
+ }.merge!(options)
+
+ if base.class.respond_to?(:i18n_scope)
+ i18n_scope = base.class.i18n_scope.to_s
+ defaults = base.class.lookup_ancestors.flat_map do |klass|
+ [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
+ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
+ end
+ defaults << :"#{i18n_scope}.errors.messages.#{type}"
+
+ catch(:exception) do
+ translation = I18n.translate(defaults.first, options.merge(default: defaults.drop(1), throw: true))
+ return translation unless translation.nil?
+ end unless options[:message]
+ else
+ defaults = []
+ end
+
+ defaults << :"errors.attributes.#{attribute}.#{type}"
+ defaults << :"errors.messages.#{type}"
+
+ key = defaults.shift
+ defaults = options.delete(:message) if options[:message]
+ options[:default] = defaults
+
+ I18n.translate(key, options)
+ end
+
def initialize(base, attribute, type = :invalid, **options)
@base = base
@attribute = attribute
@@ -28,7 +113,7 @@ module ActiveModel
def message
case raw_type
when Symbol
- base.errors.generate_message(attribute, raw_type, options.except(*CALLBACKS_OPTIONS))
+ self.class.generate_message(attribute, raw_type, @base, options.except(*CALLBACKS_OPTIONS))
else
raw_type
end
@@ -39,7 +124,7 @@ module ActiveModel
end
def full_message
- base.errors.full_message(attribute, message)
+ self.class.full_message(attribute, message, @base.class)
end
# See if error matches provided +attribute+, +type+ and +options+.
@@ -58,9 +143,9 @@ module ActiveModel
end
def strict_match?(attribute, type, **options)
- return false unless match?(attribute, type, **options)
+ return false unless match?(attribute, type)
- full_message == Error.new(@base, attribute, type, **options).full_message
+ options == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
end
def ==(other)
@@ -74,7 +159,7 @@ module ActiveModel
protected
def attributes_for_hash
- [@base, @attribute, @raw_type, @options]
+ [@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]
end
end
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index f8ea21cf47..c91ac69603 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -4,7 +4,6 @@ require "active_support/core_ext/array/conversions"
require "active_support/core_ext/string/inflections"
require "active_support/core_ext/object/deep_dup"
require "active_support/core_ext/string/filters"
-require "active_support/deprecation"
require "active_model/error"
require "active_model/nested_error"
require "forwardable"
@@ -70,11 +69,6 @@ module ActiveModel
LEGACY_ATTRIBUTES = [:messages, :details].freeze
- class << self
- attr_accessor :i18n_customize_full_message # :nodoc:
- end
- self.i18n_customize_full_message = false
-
attr_reader :errors
alias :objects :errors
@@ -106,7 +100,7 @@ module ActiveModel
def copy!(other) # :nodoc:
@errors = other.errors.deep_dup
@errors.each { |error|
- error.instance_variable_set("@base", @base)
+ error.instance_variable_set(:@base, @base)
}
end
@@ -199,7 +193,7 @@ module ActiveModel
matches.each do |error|
@errors.delete(error)
end
- matches.map(&:message)
+ matches.map(&:message).presence
end
# When passed a symbol or a name of a method, returns an array of errors
@@ -226,7 +220,7 @@ module ActiveModel
# # then yield :name and "must be specified"
# end
def each(&block)
- if block.arity == 1
+ if block.arity <= 1
@errors.each(&block)
else
ActiveSupport::Deprecation.warn(<<~MSG)
@@ -309,6 +303,16 @@ module ActiveModel
hash
end
+ def to_h
+ ActiveSupport::Deprecation.warn(<<~EOM)
+ ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2
+ Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
+ returned by `ActiveModel::Errors.to_hash` is an array of error messages.
+ EOM
+
+ to_hash.transform_values { |values| values.last }
+ end
+
def messages
DeprecationHandlingMessageHash.new(self)
end
@@ -468,47 +472,7 @@ module ActiveModel
#
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
def full_message(attribute, message)
- return message if attribute == :base
- attribute = attribute.to_s
-
- if self.class.i18n_customize_full_message && @base.class.respond_to?(:i18n_scope)
- attribute = attribute.remove(/\[\d\]/)
- parts = attribute.split(".")
- attribute_name = parts.pop
- namespace = parts.join("/") unless parts.empty?
- attributes_scope = "#{@base.class.i18n_scope}.errors.models"
-
- if namespace
- defaults = @base.class.lookup_ancestors.map do |klass|
- [
- :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
- :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
- ]
- end
- else
- defaults = @base.class.lookup_ancestors.map do |klass|
- [
- :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
- :"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
- ]
- end
- end
-
- defaults.flatten!
- else
- defaults = []
- end
-
- defaults << :"errors.format"
- defaults << "%{attribute} %{message}"
-
- attr_name = attribute.tr(".", "_").humanize
- attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
-
- I18n.t(defaults.shift,
- default: defaults,
- attribute: attr_name,
- message: message)
+ Error.full_message(attribute, message, @base.class)
end
# Translates an error message in its default scope
@@ -536,40 +500,7 @@ module ActiveModel
# * <tt>errors.attributes.title.blank</tt>
# * <tt>errors.messages.blank</tt>
def generate_message(attribute, type = :invalid, options = {})
- type = options.delete(:message) if options[:message].is_a?(Symbol)
- value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
-
- options = {
- model: @base.model_name.human,
- attribute: @base.class.human_attribute_name(attribute),
- value: value,
- object: @base
- }.merge!(options)
-
- if @base.class.respond_to?(:i18n_scope)
- i18n_scope = @base.class.i18n_scope.to_s
- defaults = @base.class.lookup_ancestors.flat_map do |klass|
- [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
- :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
- end
- defaults << :"#{i18n_scope}.errors.messages.#{type}"
-
- catch(:exception) do
- translation = I18n.translate(defaults.first, options.merge(default: defaults.drop(1), throw: true))
- return translation unless translation.nil?
- end unless options[:message]
- else
- defaults = []
- end
-
- defaults << :"errors.attributes.#{attribute}.#{type}"
- defaults << :"errors.messages.#{type}"
-
- key = defaults.shift
- defaults = options.delete(:message) if options[:message]
- options[:default] = defaults
-
- I18n.translate(key, options)
+ Error.generate_message(attribute, type, @base, options)
end
def marshal_load(array) # :nodoc:
diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb
index eb7901c7e9..65e20b9791 100644
--- a/activemodel/lib/active_model/railtie.rb
+++ b/activemodel/lib/active_model/railtie.rb
@@ -14,7 +14,7 @@ module ActiveModel
end
initializer "active_model.i18n_customize_full_message" do
- ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
+ ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
end
end
end
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 5f409326bd..c177d771ad 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -45,19 +45,19 @@ module ActiveModel
# end
#
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
- # user.save # => false, password required
+ # user.save # => false, password required
# user.password = 'mUc3m00RsqyRe'
- # user.save # => false, confirmation doesn't match
+ # user.save # => false, confirmation doesn't match
# user.password_confirmation = 'mUc3m00RsqyRe'
- # user.save # => true
+ # user.save # => true
# user.recovery_password = "42password"
- # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
- # user.save # => true
- # user.authenticate('notright') # => false
- # user.authenticate('mUc3m00RsqyRe') # => user
- # user.authenticate_recovery_password('42password') # => user
- # User.find_by(name: 'david').try(:authenticate, 'notright') # => false
- # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
+ # user.save # => true
+ # user.authenticate('notright') # => false
+ # user.authenticate('mUc3m00RsqyRe') # => user
+ # user.authenticate_recovery_password('42password') # => user
+ # User.find_by(name: 'david')&.authenticate('notright') # => false
+ # User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
def has_secure_password(attribute = :password, validations: true)
# Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
diff --git a/activemodel/lib/active_model/type/float.rb b/activemodel/lib/active_model/type/float.rb
index 36c7a5103c..435e39b2c9 100644
--- a/activemodel/lib/active_model/type/float.rb
+++ b/activemodel/lib/active_model/type/float.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/object/try"
+
module ActiveModel
module Type
class Float < Value # :nodoc:
diff --git a/activemodel/lib/active_model/type/helpers/timezone.rb b/activemodel/lib/active_model/type/helpers/timezone.rb
index cf87b9715b..b0477aec32 100644
--- a/activemodel/lib/active_model/type/helpers/timezone.rb
+++ b/activemodel/lib/active_model/type/helpers/timezone.rb
@@ -7,7 +7,7 @@ module ActiveModel
module Helpers # :nodoc: all
module Timezone
def is_utc?
- ::Time.zone_default.nil? || ::Time.zone_default =~ "UTC"
+ ::Time.zone_default.nil? || ::Time.zone_default.match?("UTC")
end
def default_timezone
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index bea57415b0..a473440b4e 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -6,7 +6,7 @@ module ActiveModel
def validate_each(record, attribute, value)
if options[:with]
regexp = option_call(record, :with)
- record_error(record, attribute, :with, value) if value.to_s !~ regexp
+ record_error(record, attribute, :with, value) unless regexp.match?(value.to_s)
elsif options[:without]
regexp = option_call(record, :without)
record_error(record, attribute, :without, value) if regexp.match?(value.to_s)
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 97612d474d..8906837aa8 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -27,7 +27,7 @@ module ActiveModel
# class EmailValidator < ActiveModel::EachValidator
# def validate_each(record, attribute, value)
# record.errors.add attribute, (options[:message] || "is not an email") unless
- # value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
+ # /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
# end
# end
#
@@ -47,7 +47,7 @@ module ActiveModel
#
# class TitleValidator < ActiveModel::EachValidator
# def validate_each(record, attribute, value)
- # record.errors.add attribute, "must start with 'the'" unless value =~ /\Athe/i
+ # record.errors.add attribute, "must start with 'the'" unless /\Athe/i.match?(value)
# end
# end
#
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index baaf404f2e..a6cd1da717 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -44,6 +44,14 @@ class ErrorsTest < ActiveModel::TestCase
assert_includes errors, "foo", "errors should include 'foo' as :foo"
end
+ def test_each_when_arity_is_negative
+ errors = ActiveModel::Errors.new(Person.new)
+ errors.add(:name, :blank)
+ errors.add(:gender, :blank)
+
+ assert_equal([:name, :gender], errors.map(&:attribute))
+ end
+
def test_any?
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name)
@@ -274,6 +282,28 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal [msg], person.errors[:name]
end
+ test "added? when attribute was added through a collection" do
+ person = Person.new
+ person.errors.add(:"family_members.name", :too_long, count: 25)
+ assert person.errors.added?(:"family_members.name", :too_long, count: 25)
+ assert_not person.errors.added?(:"family_members.name", :too_long)
+ assert_not person.errors.added?(:"family_members.name", :too_long, name: "hello")
+ end
+
+ test "added? ignores callback option" do
+ person = Person.new
+
+ person.errors.add(:name, :too_long, if: -> { true })
+ assert person.errors.added?(:name, :too_long)
+ end
+
+ test "added? ignores message option" do
+ person = Person.new
+
+ person.errors.add(:name, :too_long, message: proc { "foo" })
+ assert person.errors.added?(:name, :too_long)
+ end
+
test "added? detects indifferent if a specific error was added to the object" do
person = Person.new
person.errors.add(:name, "cannot be blank")
@@ -444,6 +474,17 @@ class ErrorsTest < ActiveModel::TestCase
assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.to_a
end
+ test "to_h is deprecated" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "too long")
+
+ expected_deprecation = "ActiveModel::Errors#to_h is deprecated"
+ assert_deprecated(expected_deprecation) do
+ assert_equal({ name: "too long" }, person.errors.to_h)
+ end
+ end
+
test "to_hash returns the error messages hash" do
person = Person.new
person.errors.add(:name, "cannot be blank")
@@ -460,6 +501,27 @@ class ErrorsTest < ActiveModel::TestCase
assert_nil person.errors.as_json.default_proc
end
+ test "full_messages doesn't require the base object to respond to `:errors" do
+ model = Class.new do
+ def initialize
+ @errors = ActiveModel::Errors.new(self)
+ @errors.add(:name, "bar")
+ end
+
+ def self.human_attribute_name(attr, options = {})
+ "foo"
+ end
+
+ def call
+ error_wrapper = Struct.new(:model_errors)
+
+ error_wrapper.new(@errors)
+ end
+ end
+
+ assert_equal(["foo bar"], model.new.call.model_errors.full_messages)
+ end
+
test "full_messages creates a list of error messages with the attribute name included" do
person = Person.new
person.errors.add(:name, "cannot be blank")
@@ -573,6 +635,12 @@ class ErrorsTest < ActiveModel::TestCase
assert_not_equal errors_dup.details, errors.details
end
+ test "delete returns nil when no errors were deleted" do
+ errors = ActiveModel::Errors.new(Person.new)
+
+ assert_nil(errors.delete(:name))
+ end
+
test "delete removes details on given attribute" do
errors = ActiveModel::Errors.new(Person.new)
errors.add(:name, :invalid)
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index a4cb472ffc..c6335ac9e3 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -10,6 +10,7 @@ I18n.enforce_available_locales = false
require "active_support/testing/autorun"
require "active_support/testing/method_call_assertions"
+require "active_support/core_ext/integer/time"
class ActiveModel::TestCase < ActiveSupport::TestCase
include ActiveSupport::Testing::MethodCallAssertions
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
index 95ee7cace3..f5ff1a3fd7 100644
--- a/activemodel/test/cases/railtie_test.rb
+++ b/activemodel/test/cases/railtie_test.rb
@@ -35,20 +35,20 @@ class RailtieTest < ActiveModel::TestCase
test "i18n customize full message defaults to false" do
@app.initialize!
- assert_equal false, ActiveModel::Errors.i18n_customize_full_message
+ assert_equal false, ActiveModel::Error.i18n_customize_full_message
end
test "i18n customize full message can be disabled" do
@app.config.active_model.i18n_customize_full_message = false
@app.initialize!
- assert_equal false, ActiveModel::Errors.i18n_customize_full_message
+ assert_equal false, ActiveModel::Error.i18n_customize_full_message
end
test "i18n customize full message can be enabled" do
@app.config.active_model.i18n_customize_full_message = true
@app.initialize!
- assert_equal true, ActiveModel::Errors.i18n_customize_full_message
+ assert_equal true, ActiveModel::Error.i18n_customize_full_message
end
end
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index b7ee50832c..c81649f493 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -13,8 +13,8 @@ class I18nValidationTest < ActiveModel::TestCase
I18n.backend = I18n::Backend::Simple.new
I18n.backend.store_translations("en", errors: { messages: { custom: nil } })
- @original_i18n_customize_full_message = ActiveModel::Errors.i18n_customize_full_message
- ActiveModel::Errors.i18n_customize_full_message = true
+ @original_i18n_customize_full_message = ActiveModel::Error.i18n_customize_full_message
+ ActiveModel::Error.i18n_customize_full_message = true
end
def teardown
@@ -24,7 +24,7 @@ class I18nValidationTest < ActiveModel::TestCase
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
I18n.backend.reload!
- ActiveModel::Errors.i18n_customize_full_message = @original_i18n_customize_full_message
+ ActiveModel::Error.i18n_customize_full_message = @original_i18n_customize_full_message
end
def test_full_message_encoding
@@ -49,7 +49,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_doesnt_use_attribute_format_without_config
- ActiveModel::Errors.i18n_customize_full_message = false
+ ActiveModel::Error.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
@@ -59,8 +59,21 @@ class I18nValidationTest < ActiveModel::TestCase
assert_equal "Name test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
end
+ def test_errors_full_messages_on_nested_error_uses_attribute_format
+ ActiveModel::Error.i18n_customize_full_message = true
+ I18n.backend.store_translations("en", activemodel: {
+ errors: { models: { person: { attributes: { gender: "Gender" } } } },
+ attributes: { "person/contacts": { gender: "Gender" } }
+ })
+
+ person = person_class.new
+ error = ActiveModel::Error.new(person, :gender, "can't be blank")
+ person.errors.import(error, attribute: "person[0].contacts.gender")
+ assert_equal ["Gender can't be blank"], person.errors.full_messages
+ end
+
def test_errors_full_messages_uses_attribute_format
- ActiveModel::Errors.i18n_customize_full_message = true
+ ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { attributes: { name: { format: "%{message}" } } } } } })
@@ -71,7 +84,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_uses_model_format
- ActiveModel::Errors.i18n_customize_full_message = true
+ ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { person: { format: "%{message}" } } } })
@@ -82,7 +95,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_uses_deeply_nested_model_attributes_format
- ActiveModel::Errors.i18n_customize_full_message = true
+ ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
@@ -93,7 +106,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_uses_deeply_nested_model_model_format
- ActiveModel::Errors.i18n_customize_full_message = true
+ ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
@@ -104,7 +117,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_attributes_format
- ActiveModel::Errors.i18n_customize_full_message = true
+ ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
@@ -115,7 +128,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_model_format
- ActiveModel::Errors.i18n_customize_full_message = true
+ ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { format: "%{message}" } } } })
@@ -126,7 +139,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_and_i18n_attribute_name
- ActiveModel::Errors.i18n_customize_full_message = true
+ ActiveModel::Error.i18n_customize_full_message = true
I18n.backend.store_translations("en", activemodel: {
attributes: { 'person/contacts/addresses': { country: "Country" } }
@@ -138,7 +151,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_with_indexed_deeply_nested_attributes_without_i18n_config
- ActiveModel::Errors.i18n_customize_full_message = false
+ ActiveModel::Error.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
errors: { models: { 'person/contacts/addresses': { attributes: { street: { format: "%{message}" } } } } } })
@@ -149,7 +162,7 @@ class I18nValidationTest < ActiveModel::TestCase
end
def test_errors_full_messages_with_i18n_attribute_name_without_i18n_config
- ActiveModel::Errors.i18n_customize_full_message = false
+ ActiveModel::Error.i18n_customize_full_message = false
I18n.backend.store_translations("en", activemodel: {
attributes: { 'person/contacts[0]/addresses[0]': { country: "Country" } }
@@ -178,8 +191,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_confirmation_of on generated message #{name}" do
person_class.validates_confirmation_of :title, validation_options
@person.title_confirmation = "foo"
- call = [:title_confirmation, :confirmation, generate_message_options.merge(attribute: "Title")]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title_confirmation, :confirmation, @person, generate_message_options.merge(attribute: "Title")]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -189,8 +202,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_acceptance_of on generated message #{name}" do
person_class.validates_acceptance_of :title, validation_options.merge(allow_nil: false)
- call = [:title, :accepted, generate_message_options]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :accepted, @person, generate_message_options]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -200,8 +213,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_presence_of on generated message #{name}" do
person_class.validates_presence_of :title, validation_options
- call = [:title, :blank, generate_message_options]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :blank, @person, generate_message_options]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -211,8 +224,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :within on generated message when too short #{name}" do
person_class.validates_length_of :title, validation_options.merge(within: 3..5)
- call = [:title, :too_short, generate_message_options.merge(count: 3)]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :too_short, @person, generate_message_options.merge(count: 3)]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -223,8 +236,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_length_of for :too_long generated message #{name}" do
person_class.validates_length_of :title, validation_options.merge(within: 3..5)
@person.title = "this title is too long"
- call = [:title, :too_long, generate_message_options.merge(count: 5)]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :too_long, @person, generate_message_options.merge(count: 5)]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -234,8 +247,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :is on generated message #{name}" do
person_class.validates_length_of :title, validation_options.merge(is: 5)
- call = [:title, :wrong_length, generate_message_options.merge(count: 5)]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :wrong_length, @person, generate_message_options.merge(count: 5)]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -246,8 +259,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_format_of on generated message #{name}" do
person_class.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/)
@person.title = "72x"
- call = [:title, :invalid, generate_message_options.merge(value: "72x")]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :invalid, @person, generate_message_options.merge(value: "72x")]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -258,8 +271,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_inclusion_of on generated message #{name}" do
person_class.validates_inclusion_of :title, validation_options.merge(in: %w(a b c))
@person.title = "z"
- call = [:title, :inclusion, generate_message_options.merge(value: "z")]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :inclusion, @person, generate_message_options.merge(value: "z")]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -270,8 +283,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_inclusion_of using :within on generated message #{name}" do
person_class.validates_inclusion_of :title, validation_options.merge(within: %w(a b c))
@person.title = "z"
- call = [:title, :inclusion, generate_message_options.merge(value: "z")]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :inclusion, @person, generate_message_options.merge(value: "z")]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -282,8 +295,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_exclusion_of generated message #{name}" do
person_class.validates_exclusion_of :title, validation_options.merge(in: %w(a b c))
@person.title = "a"
- call = [:title, :exclusion, generate_message_options.merge(value: "a")]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :exclusion, @person, generate_message_options.merge(value: "a")]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -294,8 +307,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_exclusion_of using :within generated message #{name}" do
person_class.validates_exclusion_of :title, validation_options.merge(within: %w(a b c))
@person.title = "a"
- call = [:title, :exclusion, generate_message_options.merge(value: "a")]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :exclusion, @person, generate_message_options.merge(value: "a")]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -306,8 +319,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_numericality_of generated message #{name}" do
person_class.validates_numericality_of :title, validation_options
@person.title = "a"
- call = [:title, :not_a_number, generate_message_options.merge(value: "a")]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :not_a_number, @person, generate_message_options.merge(value: "a")]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -318,8 +331,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_numericality_of for :only_integer on generated message #{name}" do
person_class.validates_numericality_of :title, validation_options.merge(only_integer: true)
@person.title = "0.0"
- call = [:title, :not_an_integer, generate_message_options.merge(value: "0.0")]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :not_an_integer, @person, generate_message_options.merge(value: "0.0")]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -330,8 +343,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_numericality_of for :odd on generated message #{name}" do
person_class.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true)
@person.title = 0
- call = [:title, :odd, generate_message_options.merge(value: 0)]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :odd, @person, generate_message_options.merge(value: 0)]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
@@ -342,8 +355,8 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_numericality_of for :less_than on generated message #{name}" do
person_class.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0)
@person.title = 1
- call = [:title, :less_than, generate_message_options.merge(value: 1, count: 0)]
- assert_called_with(@person.errors, :generate_message, call) do
+ call = [:title, :less_than, @person, generate_message_options.merge(value: 1, count: 0)]
+ assert_called_with(ActiveModel::Error, :generate_message, call) do
@person.valid?
@person.errors.messages
end
diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb
index daad76759f..6669d028a0 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_support/all"
require "models/topic"
require "models/person"
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 8642227a2b..56a8796318 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,41 @@
+* Preserve user supplied joins order as much as possible.
+
+ Fixes #36761, #34328, #24281, #12953.
+
+ *Ryuta Kamizono*
+
+* Allow `matches_regex` and `does_not_match_regexp` on the MySQL Arel visitor.
+
+ *James Pearson*
+
+* Allow specifying fixtures to be ignored by setting `ignore` in YAML file's '_fixture' section.
+
+ *Tongfei Gao*
+
+* Make the DATABASE_URL env variable only affect the primary connection. Add new env variables for multiple databases.
+
+ *John Crepezzi*, *Eileen Uchitelle*
+
+* Add a warning for enum elements with 'not_' prefix.
+
+ class Foo
+ enum status: [:sent, :not_sent]
+ end
+
+ *Edu Depetris*
+
+* Make currency symbols optional for money column type in PostgreSQL
+
+ *Joel Schneider*
+
+* Add support for beginless ranges, introduced in Ruby 2.7.
+
+ *Josh Goodall*
+
+* Add database_exists? method to connection adapters to check if a database exists.
+
+ *Guilherme Mansur*
+
* Loading the schema for a model that has no `table_name` raises a `TableNotSpecified` error.
*Guilherme Mansur*, *Eugene Kenny*
@@ -36,5 +74,4 @@
*Michael Duchemin*
-
Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index cf22b850b9..705a5571ee 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -56,6 +56,10 @@ module ActiveRecord
@inversed = false
end
+ def reset_negative_cache # :nodoc:
+ reset if loaded? && target.nil?
+ end
+
# Reloads the \target and returns +self+ on success.
# The QueryCache is cleared if +force+ is true.
def reload(force = false)
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index 3346725f2d..e3886f394a 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -44,7 +44,7 @@ module ActiveRecord
def decrement_counters_before_last_save
if reflection.polymorphic?
- model_was = owner.attribute_before_last_save(reflection.foreign_type).try(:constantize)
+ model_was = owner.attribute_before_last_save(reflection.foreign_type)&.constantize
else
model_was = klass
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 6ad4c75fb5..2072f93194 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -62,7 +62,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
def middle_reflection(join_model)
middle_name = [lhs_model.name.downcase.pluralize,
- association_name].join("_").gsub("::", "_").to_sym
+ association_name.to_s].sort.join("_").gsub("::", "_").to_sym
middle_options = middle_options join_model
HasMany.create_reflection(lhs_model,
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 27ebe8cb71..db8393930e 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -32,15 +32,12 @@ module ActiveRecord::Associations::Builder # :nodoc:
end
end
- def self.touch_record(o, name, touch)
- record = o.send name
+ def self.touch_record(record, name, touch)
+ instance = record.send(name)
- return unless record && record.persisted?
-
- if touch != true
- record.touch(touch)
- else
- record.touch
+ if instance&.persisted?
+ touch != true ?
+ instance.touch(touch) : instance.touch
end
end
@@ -48,11 +45,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
name = reflection.name
touch = reflection.options[:touch]
- callback = lambda { |record|
- HasOne.touch_record(record, name, touch)
- }
-
+ callback = -> (record) { HasOne.touch_record(record, name, touch) }
model.after_create callback, if: :saved_changes?
+ model.after_create_commit { association(name).reset_negative_cache }
model.after_update callback, if: :saved_changes?
model.after_destroy callback
model.after_touch callback
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index c3d4eab562..891b50d160 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -56,7 +56,7 @@ module ActiveRecord
def ids_writer(ids)
primary_key = reflection.association_primary_key
pk_type = klass.type_for_attribute(primary_key)
- ids = Array(ids).reject(&:blank?)
+ ids = Array(ids).compact_blank
ids.map! { |i| pk_type.cast(i) }
records = klass.where(primary_key => ids).index_by do |r|
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 0db0ad8595..404a7c02ba 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -100,7 +100,7 @@ module ActiveRecord
# converting them into an array and iterating through them using
# Array#select.
#
- # person.pets.select { |pet| pet.name =~ /oo/ }
+ # person.pets.select { |pet| /oo/.match?(pet.name) }
# # => [
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 0cf67644af..5a21e36cc7 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -17,7 +17,7 @@ module ActiveRecord
when false, nil then false
else
if !type_for_attribute(attr_name) { false }
- if Numeric === value || value !~ /[^0-9]/
+ if Numeric === value || !value.match?(/[^0-9]/)
!value.to_i.zero?
else
return false if ActiveModel::Type::Boolean::FALSE_VALUES.include?(value)
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index fb44232dff..9a4de1a034 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/object/try"
+
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index a7709b444d..734ebb45ae 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -302,7 +302,7 @@ module ActiveRecord
def validate_single_association(reflection)
association = association_instance_get(reflection.name)
record = association && association.reader
- association_valid?(reflection, record) if record
+ association_valid?(reflection, record) if record && record.changed_for_autosave?
end
# Validate the associated records if <tt>:validate</tt> or
@@ -409,7 +409,7 @@ module ActiveRecord
saved = record.save(validate: false)
end
- raise ActiveRecord::Rollback unless saved
+ raise(RecordInvalid.new(association.owner)) unless saved
end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 2af6d09b53..282c9fcf30 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -12,7 +12,6 @@ require "active_support/core_ext/hash/slice"
require "active_support/core_ext/string/behavior"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/module/introspection"
-require "active_support/core_ext/object/duplicable"
require "active_support/core_ext/class/subclasses"
require "active_record/attribute_decorators"
require "active_record/define_callbacks"
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 9b3f5260f7..276d5a25d4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -426,7 +426,7 @@ module ActiveRecord
# #connection can be called any number of times; the connection is
# held in a cache keyed by a thread.
def connection
- @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout
+ @thread_cached_conns[connection_cache_key(current_thread)] ||= checkout
end
# Returns true if there is an open connection being used for the current thread.
@@ -435,7 +435,7 @@ module ActiveRecord
# #connection or #with_connection methods. Connections obtained through
# #checkout will not be detected by #active_connection?
def active_connection?
- @thread_cached_conns[connection_cache_key(Thread.current)]
+ @thread_cached_conns[connection_cache_key(current_thread)]
end
# Signal that the thread is finished with the current connection.
@@ -730,6 +730,10 @@ module ActiveRecord
thread
end
+ def current_thread
+ @lock_thread || Thread.current
+ end
+
# Take control of all existing connections so a "group" action such as
# reload/disconnect can be performed safely. It is no longer enough to
# wrap it in +synchronize+ because some pool's actions are allowed
@@ -1016,8 +1020,8 @@ module ActiveRecord
# In some cases you may want to prevent writes to the database
# even if you are on a database that can write. `while_preventing_writes`
# will prevent writes to the database for the duration of the block.
- def while_preventing_writes
- original, @prevent_writes = @prevent_writes, true
+ def while_preventing_writes(enabled = true)
+ original, @prevent_writes = @prevent_writes, enabled
yield
ensure
@prevent_writes = original
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 75e959045e..d932f068f2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/deprecation"
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
module DatabaseLimits
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index 6fec4dbd81..768122b4d2 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -33,17 +33,17 @@ module ActiveRecord
end
def enable_query_cache!
- @query_cache_enabled[connection_cache_key(Thread.current)] = true
+ @query_cache_enabled[connection_cache_key(current_thread)] = true
connection.enable_query_cache! if active_connection?
end
def disable_query_cache!
- @query_cache_enabled.delete connection_cache_key(Thread.current)
+ @query_cache_enabled.delete connection_cache_key(current_thread)
connection.disable_query_cache! if active_connection?
end
def query_cache_enabled
- @query_cache_enabled[connection_cache_key(Thread.current)]
+ @query_cache_enabled[connection_cache_key(current_thread)]
end
end
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 dbd533b4b3..aedd8119ef 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -221,7 +221,7 @@ module ActiveRecord
end
class_methods do
- private def define_column_methods(*column_types) # :nodoc:
+ def define_column_methods(*column_types) # :nodoc:
column_types.each do |column_type|
module_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{column_type}(*names, **options)
@@ -231,6 +231,7 @@ module ActiveRecord
RUBY
end
end
+ private :define_column_methods
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 79dc98607a..88367c79a1 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -2,7 +2,6 @@
require "active_record/migration/join_table"
require "active_support/core_ext/string/access"
-require "active_support/deprecation"
require "digest/sha2"
module ActiveRecord
@@ -101,7 +100,7 @@ module ActiveRecord
def index_exists?(table_name, column_name, options = {})
column_names = Array(column_name).map(&:to_s)
checks = []
- checks << lambda { |i| i.columns == column_names }
+ checks << lambda { |i| Array(i.columns) == column_names }
checks << lambda { |i| i.unique } if options[:unique]
checks << lambda { |i| i.name == options[:name].to_s } if options[:name]
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index c0ead17b3b..dc970c384b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -6,7 +6,6 @@ require "active_record/connection_adapters/sql_type_metadata"
require "active_record/connection_adapters/abstract/schema_dumper"
require "active_record/connection_adapters/abstract/schema_creation"
require "active_support/concurrency/load_interlock_aware_monitor"
-require "active_support/deprecation"
require "arel/collectors/bind"
require "arel/collectors/composite"
require "arel/collectors/sql_string"
@@ -106,6 +105,14 @@ module ActiveRecord
Regexp.union(*parts)
end
+ def self.quoted_column_names # :nodoc:
+ @quoted_column_names ||= {}
+ end
+
+ def self.quoted_table_names # :nodoc:
+ @quoted_table_names ||= {}
+ end
+
def initialize(connection, logger = nil, config = {}) # :nodoc:
super()
@@ -116,7 +123,6 @@ module ActiveRecord
@config = config
@pool = ActiveRecord::ConnectionAdapters::NullPool.new
@idle_since = Concurrent.monotonic_time
- @quoted_column_names, @quoted_table_names = {}, {}
@visitor = arel_visitor
@statements = build_statement_pool
@lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
@@ -264,6 +270,11 @@ module ActiveRecord
self.class::ADAPTER_NAME
end
+ # Does the database for this adapter exist?
+ def self.database_exists?(config)
+ raise NotImplementedError
+ end
+
# Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
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 70292e6d63..0fe16270ed 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -476,12 +476,12 @@ module ActiveRecord
# distinct queries, and requires that the ORDER BY include the distinct column.
# See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html
def columns_for_distinct(columns, orders) # :nodoc:
- order_columns = orders.reject(&:blank?).map { |s|
+ order_columns = orders.compact_blank.map { |s|
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
- }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
+ }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" }
(order_columns << super).join(", ")
end
@@ -572,7 +572,8 @@ module ActiveRecord
end
end
- # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
+ # See https://dev.mysql.com/doc/refman/5.7/en/server-error-reference.html
+ ER_DB_CREATE_EXISTS = 1007
ER_DUP_ENTRY = 1062
ER_NOT_NULL_VIOLATION = 1048
ER_NO_REFERENCED_ROW = 1216
@@ -592,6 +593,8 @@ module ActiveRecord
def translate_exception(exception, message:, sql:, binds:)
case error_number(exception)
+ when ER_DB_CREATE_EXISTS
+ DatabaseAlreadyExists.new(message, sql: sql, binds: binds)
when ER_DUP_ENTRY
RecordNotUnique.new(message, sql: sql, binds: binds)
when ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED, ER_ROW_IS_REFERENCED_2, ER_NO_REFERENCED_ROW_2
@@ -619,7 +622,11 @@ module ActiveRecord
when ER_QUERY_INTERRUPTED
QueryCanceled.new(message, sql: sql, binds: binds)
else
- super
+ if exception.is_a?(Mysql2::Error::TimeoutError)
+ ActiveRecord::AdapterTimeout.new(message, sql: sql, binds: binds)
+ else
+ super
+ end
end
end
@@ -737,7 +744,7 @@ module ActiveRecord
end.compact.join(", ")
# ...and send them all in one query
- execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
+ execute("SET #{encoding} #{sql_mode_assignment} #{variable_assignments}", "SCHEMA")
end
def column_definitions(table_name) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index df26f67c6e..20041f0c85 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -50,7 +50,7 @@ module ActiveRecord
# Converts the given URL to a full connection hash.
def to_hash
- config = raw_config.reject { |_, value| value.blank? }
+ config = raw_config.compact_blank
config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String }
config
end
@@ -275,7 +275,7 @@ module ActiveRecord
# hash and merges with the rest of the hash.
# Connection details inside of the "url" key win any merge conflicts
def resolve_hash_connection(spec)
- if spec["url"] && spec["url"] !~ /^jdbc:/
+ if spec["url"] && !spec["url"].match?(/^jdbc:/)
connection_hash = resolve_url_connection(spec.delete("url"))
spec.merge!(connection_hash)
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
index dfed5471f4..0069f5871c 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -5,11 +5,11 @@ module ActiveRecord
module MySQL
module Quoting # :nodoc:
def quote_column_name(name)
- @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
+ self.class.quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
end
def quote_table_name(name)
- @quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
+ self.class.quoted_table_names[name] ||= super.gsub(".", "`.`").freeze
end
def unquoted_true
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index 234fb25fdf..bcd300f3db 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -41,13 +41,15 @@ module ActiveRecord
case column.sql_type
when /\Atimestamp\b/
:timestamp
+ when /\A(?:enum|set)\b/
+ column.sql_type
else
super
end
end
def schema_limit(column)
- super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type)
+ super unless /\A(?:enum|set|(?:tiny|medium|long)?(?:text|blob))\b/.match?(column.sql_type)
end
def schema_precision(column)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 53510c62c2..1df9ac32c9 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -8,6 +8,8 @@ require "mysql2"
module ActiveRecord
module ConnectionHandling # :nodoc:
+ ER_BAD_DB_ERROR = 1049
+
# Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config)
config = config.symbolize_keys
@@ -22,7 +24,7 @@ module ActiveRecord
client = Mysql2::Client.new(config)
ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config)
rescue Mysql2::Error => error
- if error.message.include?("Unknown database")
+ if error.error_number == ER_BAD_DB_ERROR
raise ActiveRecord::NoDatabaseError
else
raise
@@ -42,6 +44,12 @@ module ActiveRecord
configure_connection
end
+ def self.database_exists?(config)
+ !!ActiveRecord::Base.mysql2_connection(config)
+ rescue ActiveRecord::NoDatabaseError
+ false
+ end
+
def supports_json?
!mariadb? && database_version >= "5.7.8"
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index b4ea6c42d3..f1ecf6df30 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -24,6 +24,16 @@ module ActiveRecord
super.sub(/\[\]\z/, "")
end
+ def init_with(coder)
+ @serial = coder["serial"]
+ super
+ end
+
+ def encode_with(coder)
+ coder["serial"] = @serial
+ super
+ end
+
def ==(other)
other.is_a?(Column) &&
super &&
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
index 6434377b57..357493dfc0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -26,9 +26,9 @@ module ActiveRecord
value = value.sub(/^\((.+)\)$/, '-\1') # (4)
case value
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
+ when /^-?\D*[\d,]+\.\d{2}$/ # (1)
value.gsub!(/[^-\d.]/, "")
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ when /^-?\D*[\d.]+,\d{2}$/ # (2)
value.gsub!(/[^-\d,]/, "").sub!(/,/, ".")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 0c800dca83..07b66de366 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -30,7 +30,7 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name) # :nodoc:
- @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
+ self.class.quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted.freeze
end
# Quotes schema names for use in SQL queries.
@@ -44,7 +44,7 @@ module ActiveRecord
# Quotes column names for use in SQL queries.
def quote_column_name(name) # :nodoc:
- @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
+ self.class.quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze
end
# Quote date/time values for use in SQL input.
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
index 8df91c988b..75213e964b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -24,7 +24,7 @@ WARNING: Rails was not able to disable referential integrity.
This is most likely caused due to missing permissions.
Rails needs superuser privileges to disable referential integrity.
- cause: #{original_exception.try(:message)}
+ cause: #{original_exception&.message}
WARNING
raise e
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 0062952667..628a609521 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -555,13 +555,13 @@ module ActiveRecord
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
def columns_for_distinct(columns, orders) #:nodoc:
- order_columns = orders.reject(&:blank?).map { |s|
+ order_columns = orders.compact_blank.map { |s|
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
- }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
+ }.compact_blank.map.with_index { |column, i| "#{column} AS alias_#{i}" }
(order_columns << super).join(", ")
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 6b18a12bce..d6758c9d4b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -12,6 +12,7 @@ class ::PG::Connection # :nodoc:
end
end
+require "active_support/core_ext/object/try"
require "active_record/connection_adapters/abstract_adapter"
require "active_record/connection_adapters/statement_pool"
require "active_record/connection_adapters/postgresql/column"
@@ -46,7 +47,7 @@ module ActiveRecord
conn = PG.connect(conn_params)
ConnectionAdapters::PostgreSQLAdapter.new(conn, logger, conn_params, config)
rescue ::PG::Error => error
- if error.message.include?("does not exist")
+ if error.message.include?(conn_params[:dbname])
raise ActiveRecord::NoDatabaseError
else
raise
@@ -259,6 +260,12 @@ module ActiveRecord
@use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
+ def self.database_exists?(config)
+ !!ActiveRecord::Base.postgresql_connection(config)
+ rescue ActiveRecord::NoDatabaseError
+ false
+ end
+
# Is this connection alive and ready for queries?
def active?
@lock.synchronize do
@@ -461,6 +468,7 @@ module ActiveRecord
UNIQUE_VIOLATION = "23505"
SERIALIZATION_FAILURE = "40001"
DEADLOCK_DETECTED = "40P01"
+ DUPLICATE_DATABASE = "42P04"
LOCK_NOT_AVAILABLE = "55P03"
QUERY_CANCELED = "57014"
@@ -482,6 +490,8 @@ module ActiveRecord
SerializationFailure.new(message, sql: sql, binds: binds)
when DEADLOCK_DETECTED
Deadlocked.new(message, sql: sql, binds: binds)
+ when DUPLICATE_DATABASE
+ DatabaseAlreadyExists.new(message, sql: sql, binds: binds)
when LOCK_NOT_AVAILABLE
LockWaitTimeout.new(message, sql: sql, binds: binds)
when QUERY_CANCELED
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 7d54fcf9a0..5e30304864 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -46,7 +46,11 @@ module ActiveRecord
end
def primary_keys(table_name)
- @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil
+ @primary_keys.fetch(table_name) do
+ if data_source_exists?(table_name)
+ @primary_keys[deep_deduplicate(table_name)] = deep_deduplicate(connection.primary_key(table_name))
+ end
+ end
end
# A cached lookup for table existence.
@@ -54,7 +58,7 @@ module ActiveRecord
prepare_data_sources if @data_sources.empty?
return @data_sources[name] if @data_sources.key? name
- @data_sources[name] = connection.data_source_exists?(name)
+ @data_sources[deep_deduplicate(name)] = connection.data_source_exists?(name)
end
# Add internal cache for table with +table_name+.
@@ -73,13 +77,17 @@ module ActiveRecord
# Get the columns for a table
def columns(table_name)
- @columns[table_name] ||= connection.columns(table_name)
+ @columns.fetch(table_name) do
+ @columns[deep_deduplicate(table_name)] = deep_deduplicate(connection.columns(table_name))
+ end
end
# Get the columns for a table as a hash, key is the column name
# value is the column object.
def columns_hash(table_name)
- @columns_hash[table_name] ||= columns(table_name).index_by(&:name)
+ @columns_hash.fetch(table_name) do
+ @columns_hash[deep_deduplicate(table_name)] = columns(table_name).index_by(&:name)
+ end
end
# Checks whether the columns hash is already cached for a table.
@@ -88,7 +96,9 @@ module ActiveRecord
end
def indexes(table_name)
- @indexes[table_name] ||= connection.indexes(table_name)
+ @indexes.fetch(table_name) do
+ @indexes[deep_deduplicate(table_name)] = deep_deduplicate(connection.indexes(table_name))
+ end
end
def database_version # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index 58787cf9db..9b74a774e5 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -13,11 +13,11 @@ module ActiveRecord
end
def quote_table_name(name)
- @quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze
+ self.class.quoted_table_names[name] ||= super.gsub(".", "\".\"").freeze
end
def quote_column_name(name)
- @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
+ self.class.quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
end
def quoted_time(value)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index da971fdba7..f4847eb6c0 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -98,6 +98,16 @@ module ActiveRecord
configure_connection
end
+ def self.database_exists?(config)
+ config = config.symbolize_keys
+ if config[:database] == ":memory:"
+ return true
+ else
+ database_file = defined?(Rails.root) ? File.expand_path(config[:database], Rails.root) : config[:database]
+ File.exist?(database_file)
+ end
+ end
+
def supports_ddl_transactions?
true
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 595ef4ee25..1b2c9c6754 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -45,7 +45,7 @@ module ActiveRecord
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development",
# @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>,
# #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production",
- # @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}>
+ # @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/production.sqlite3"}>
# ]>
def self.configurations=(config)
@@configurations = ActiveRecord::DatabaseConfigurations.new(config)
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index 44b5cfc738..268be34b21 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -9,6 +9,8 @@ module ActiveRecord
# objects (either a HashConfig or UrlConfig) that are constructed from the
# application's database configuration hash or URL string.
class DatabaseConfigurations
+ class InvalidConfigurationError < StandardError; end
+
attr_reader :configurations
delegate :any?, to: :configurations
@@ -91,6 +93,19 @@ module ActiveRecord
end
alias :blank? :empty?
+ def each
+ throw_getter_deprecation(:each)
+ configurations.each { |config|
+ yield [config.env_name, config.config]
+ }
+ end
+
+ def first
+ throw_getter_deprecation(:first)
+ config = configurations.first
+ [config.env_name, config.config]
+ end
+
private
def env_with_configs(env = nil)
if env
@@ -104,34 +119,48 @@ module ActiveRecord
return configs.configurations if configs.is_a?(DatabaseConfigurations)
return configs if configs.is_a?(Array)
- build_db_config = configs.each_pair.flat_map do |env_name, config|
- walk_configs(env_name.to_s, "primary", config)
- end.flatten.compact
+ db_configs = configs.flat_map do |env_name, config|
+ if config.is_a?(Hash) && config.all? { |_, v| v.is_a?(Hash) }
+ walk_configs(env_name.to_s, config)
+ else
+ build_db_config_from_raw_config(env_name.to_s, "primary", config)
+ end
+ end
+
+ current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
- if url = ENV["DATABASE_URL"]
- build_url_config(url, build_db_config)
- else
- build_db_config
+ unless db_configs.find(&:for_current_env?)
+ db_configs << environment_url_config(current_env, "primary", {})
+ end
+
+ merge_db_environment_variables(current_env, db_configs.compact)
+ end
+
+ def walk_configs(env_name, config)
+ config.map do |spec_name, sub_config|
+ build_db_config_from_raw_config(env_name, spec_name.to_s, sub_config)
end
end
- def walk_configs(env_name, spec_name, config)
+ def build_db_config_from_raw_config(env_name, spec_name, config)
case config
when String
build_db_config_from_string(env_name, spec_name, config)
when Hash
build_db_config_from_hash(env_name, spec_name, config.stringify_keys)
+ else
+ raise InvalidConfigurationError, "'{ #{env_name} => #{config} }' is not a valid configuration. Expected '#{config}' to be a URL string or a Hash."
end
end
def build_db_config_from_string(env_name, spec_name, config)
url = config
uri = URI.parse(url)
- if uri.try(:scheme)
+ if uri.scheme
ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url)
+ else
+ raise InvalidConfigurationError, "'{ #{env_name} => #{config} }' is not a valid configuration. Expected '#{config}' to be a URL string or a Hash."
end
- rescue URI::InvalidURIError
- ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
end
def build_db_config_from_hash(env_name, spec_name, config)
@@ -141,36 +170,36 @@ module ActiveRecord
config_without_url.delete "url"
ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url)
- elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String })
- ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
else
- config.each_pair.map do |sub_spec_name, sub_config|
- walk_configs(env_name, sub_spec_name, sub_config)
- end
+ ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config)
end
end
- def build_url_config(url, configs)
- env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s
+ def merge_db_environment_variables(current_env, configs)
+ configs.map do |config|
+ next config if config.url_config? || config.env_name != current_env
- if original_config = configs.find(&:for_current_env?)
- if original_config.url_config?
- configs
- else
- configs.map do |config|
- ActiveRecord::DatabaseConfigurations::UrlConfig.new(config.env_name, config.spec_name, url, config.config)
- end
- end
- else
- configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)]
+ url_config = environment_url_config(current_env, config.spec_name, config.config)
+ url_config || config
end
end
+ def environment_url_config(env, spec_name, config)
+ url = environment_value_for(spec_name)
+ return unless url
+
+ ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, spec_name, url, config)
+ end
+
+ def environment_value_for(spec_name)
+ spec_env_key = "#{spec_name.upcase}_DATABASE_URL"
+ url = ENV[spec_env_key]
+ url ||= ENV["DATABASE_URL"] if spec_name == "primary"
+ url
+ end
+
def method_missing(method, *args, &blk)
case method
- when :each, :first
- throw_getter_deprecation(method)
- configurations.send(method, *args, &blk)
when :fetch
throw_getter_deprecation(method)
configs_for(env_name: args.first)
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 8077630aeb..fc49f752aa 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -200,6 +200,8 @@ module ActiveRecord
# scope :active, -> { where(status: 0) }
# scope :not_active, -> { where.not(status: 0) }
if enum_scopes != false
+ klass.send(:detect_negative_condition!, value_method_name)
+
klass.send(:detect_enum_conflict!, name, value_method_name, true)
klass.scope value_method_name, -> { where(attr => value) }
@@ -261,5 +263,12 @@ module ActiveRecord
source: source
}
end
+
+ def detect_negative_condition!(method_name)
+ if method_name.start_with?("not_") && logger
+ logger.warn "An enum element in #{self.name} uses the prefix 'not_'." \
+ " This will cause a conflict with auto generated negative scopes."
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index c8c06375a3..0f110b4536 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -106,7 +106,7 @@ module ActiveRecord
# Wraps the underlying database error as +cause+.
class StatementInvalid < ActiveRecordError
def initialize(message = nil, sql: nil, binds: nil)
- super(message || $!.try(:message))
+ super(message || $!&.message)
@sql = sql
@binds = binds
end
@@ -187,6 +187,10 @@ module ActiveRecord
class NoDatabaseError < StatementInvalid
end
+ # Raised when creating a database if it exists.
+ class DatabaseAlreadyExists < StatementInvalid
+ end
+
# Raised when PostgreSQL returns 'cached plan must not change result type' and
# we cannot retry gracefully (e.g. inside a transaction)
class PreparedStatementCacheExpired < StatementInvalid
@@ -334,7 +338,7 @@ module ActiveRecord
# See the following:
#
# * https://www.postgresql.org/docs/current/static/transaction-iso.html
- # * https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html#error_er_lock_deadlock
+ # * https://dev.mysql.com/doc/refman/5.7/en/server-error-reference.html#error_er_lock_deadlock
class TransactionRollbackError < StatementInvalid
end
@@ -353,16 +357,24 @@ module ActiveRecord
class IrreversibleOrderError < ActiveRecordError
end
+ # Superclass for errors that have been aborted (either by client or server).
+ class QueryAborted < StatementInvalid
+ end
+
# LockWaitTimeout will be raised when lock wait timeout exceeded.
class LockWaitTimeout < StatementInvalid
end
# StatementTimeout will be raised when statement timeout exceeded.
- class StatementTimeout < StatementInvalid
+ class StatementTimeout < QueryAborted
end
# QueryCanceled will be raised when canceling statement due to user request.
- class QueryCanceled < StatementInvalid
+ class QueryCanceled < QueryAborted
+ end
+
+ # AdapterTimeout will be raised when database clients times out while waiting from the server.
+ class AdapterTimeout < QueryAborted
end
# UnknownAttributeReference is raised when an unknown and potentially unsafe
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index a86217abc0..ce209092f5 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -26,7 +26,7 @@ module ActiveRecord
payload[:exception] ||
payload[:cached] ||
IGNORED_PAYLOADS.include?(payload[:name]) ||
- payload[:sql] !~ EXPLAINED_SQLS
+ !payload[:sql].match?(EXPLAINED_SQLS)
end
ActiveSupport::Notifications.subscribe("sql.active_record", new)
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index f1ea0e022f..b2030a5bb9 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -29,6 +29,10 @@ module ActiveRecord
config_row["model_class"]
end
+ def ignored_fixtures
+ config_row["ignore"]
+ end
+
private
def rows
@rows ||= raw_rows.reject { |fixture_name, _| fixture_name == "_fixture" }
@@ -40,7 +44,7 @@ module ActiveRecord
if row
row.last
else
- { 'model_class': nil }
+ { 'model_class': nil, 'ignore': nil }
end
end
end
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 046ed0e95c..e4b958f1b9 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -420,6 +420,29 @@ module ActiveRecord
#
# Any fixture labeled "DEFAULTS" is safely ignored.
#
+ # Besides using "DEFAULTS", you can also specify what fixtures will
+ # be ignored by setting "ignore" in "_fixture" section.
+ #
+ # # users.yml
+ # _fixture:
+ # ignore:
+ # - base
+ # # or use "ignore: base" when there is only one fixture needs to be ignored.
+ #
+ # base: &base
+ # admin: false
+ # introduction: "This is a default description"
+ #
+ # admin:
+ # <<: *base
+ # admin: true
+ #
+ # visitor:
+ # <<: *base
+ #
+ # In the above example, 'base' will be ignored when creating fixtures.
+ # This can be used for common attributes inheriting.
+ #
# == Configure the fixture model class
#
# It's possible to set the fixture's model class directly in the YAML file.
@@ -530,15 +553,15 @@ module ActiveRecord
end
end
- def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
+ def create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base, &block)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
class_names = ClassCache.new class_names, config
# FIXME: Apparently JK uses this.
- connection = block_given? ? yield : ActiveRecord::Base.connection
+ connection = block_given? ? block : lambda { ActiveRecord::Base.connection }
fixture_files_to_read = fixture_set_names.reject do |fs_name|
- fixture_is_cached?(connection, fs_name)
+ fixture_is_cached?(connection.call, fs_name)
end
if fixture_files_to_read.any?
@@ -548,9 +571,9 @@ module ActiveRecord
class_names,
connection,
)
- cache_fixtures(connection, fixtures_map)
+ cache_fixtures(connection.call, fixtures_map)
end
- cached_fixtures(connection, fixture_set_names)
+ cached_fixtures(connection.call, fixture_set_names)
end
# Returns a consistent, platform-independent identifier for +label+.
@@ -589,7 +612,11 @@ module ActiveRecord
def insert(fixture_sets, connection) # :nodoc:
fixture_sets_by_connection = fixture_sets.group_by do |fixture_set|
- fixture_set.model_class&.connection || connection
+ if fixture_set.model_class
+ fixture_set.model_class.connection
+ else
+ connection.call
+ end
end
fixture_sets_by_connection.each do |conn, set|
@@ -600,6 +627,7 @@ module ActiveRecord
table_rows_for_connection[table].unshift(*rows)
end
end
+
conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys)
# Cap primary key sequences to max(pk).
@@ -614,7 +642,7 @@ module ActiveRecord
end
end
- attr_reader :table_name, :name, :fixtures, :model_class, :config
+ attr_reader :table_name, :name, :fixtures, :model_class, :ignored_fixtures, :config
def initialize(_, name, class_name, path, config = ActiveRecord::Base)
@name = name
@@ -647,8 +675,8 @@ module ActiveRecord
# Returns a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- # allow a standard key to be used for doing defaults in YAML
- fixtures.delete("DEFAULTS")
+ # allow specifying fixtures to be ignored by setting `ignore` in `_fixture` section
+ fixtures.except!(*ignored_fixtures)
TableRows.new(
table_name,
@@ -667,6 +695,21 @@ module ActiveRecord
end
end
+ def ignored_fixtures=(base)
+ @ignored_fixtures =
+ case base
+ when Array
+ base
+ when String
+ [base]
+ else
+ []
+ end
+
+ @ignored_fixtures << "DEFAULTS" unless @ignored_fixtures.include?("DEFAULTS")
+ @ignored_fixtures.compact
+ end
+
# Loads the fixtures from the YAML file at +path+.
# If the file sets the +model_class+ and current instance value is not set,
# it uses the file value.
@@ -678,6 +721,7 @@ module ActiveRecord
yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
self.model_class ||= fh.model_class if fh.model_class
+ self.ignored_fixtures ||= fh.ignored_fixtures
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
end
diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
index f6577dcbc4..3863b6bfea 100644
--- a/activerecord/lib/active_record/insert_all.rb
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -14,7 +14,7 @@ module ActiveRecord
@returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
@returning = false if @returning == []
- @unique_by = find_unique_index_for(unique_by) if unique_by
+ @unique_by = find_unique_index_for(unique_by || model.primary_key)
@on_duplicate = :skip if @on_duplicate == :update && updatable_columns.empty?
ensure_valid_options_for_connection!
@@ -32,7 +32,7 @@ module ActiveRecord
end
def primary_keys
- Array(model.primary_key)
+ Array(connection.schema_cache.primary_keys(model.table_name))
end
@@ -61,6 +61,8 @@ module ActiveRecord
if index = unique_indexes.find { |i| match.include?(i.name) || i.columns == match }
index
+ elsif match == primary_keys
+ nil
else
raise ArgumentError, "No unique index found for #{unique_by}"
end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
index 3eb1039c50..b2aa453a8e 100644
--- a/activerecord/lib/active_record/middleware/database_selector/resolver.rb
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
@@ -46,7 +46,7 @@ module ActiveRecord
private
def read_from_primary(&blk)
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
- ActiveRecord::Base.connection_handler.while_preventing_writes do
+ ActiveRecord::Base.connection_handler.while_preventing_writes(true) do
instrumenter.instrument("database_selector.active_record.read_from_primary") do
yield
end
@@ -64,10 +64,12 @@ module ActiveRecord
def write_to_primary(&blk)
ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
- instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
- yield
- ensure
- context.update_last_write_timestamp
+ ActiveRecord::Base.connection_handler.while_preventing_writes(false) do
+ instrumenter.instrument("database_selector.active_record.wrote_to_primary") do
+ yield
+ ensure
+ context.update_last_write_timestamp
+ end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index cab2369b71..ab107742ed 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -2,7 +2,6 @@
require "active_support/core_ext/hash/except"
require "active_support/core_ext/module/redefine_method"
-require "active_support/core_ext/object/try"
require "active_support/core_ext/hash/indifferent_access"
module ActiveRecord
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 323b01ab2d..4dfe8655fe 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -566,10 +566,10 @@ module ActiveRecord
def becomes(klass)
became = klass.allocate
became.send(:initialize)
- became.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@mutations_from_database", @mutations_from_database ||= nil)
- became.instance_variable_set("@new_record", new_record?)
- became.instance_variable_set("@destroyed", destroyed?)
+ became.instance_variable_set(:@attributes, @attributes)
+ became.instance_variable_set(:@mutations_from_database, @mutations_from_database ||= nil)
+ became.instance_variable_set(:@new_record, new_record?)
+ became.instance_variable_set(:@destroyed, destroyed?)
became.errors.copy!(errors)
became
end
@@ -809,7 +809,7 @@ module ActiveRecord
self.class.unscoped { self.class.find(id) }
end
- @attributes = fresh_object.instance_variable_get("@attributes")
+ @attributes = fresh_object.instance_variable_get(:@attributes)
@new_record = false
self
end
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 43a21e629e..881ba623b8 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -29,10 +29,10 @@ module ActiveRecord
pools = []
ActiveRecord::Base.connection_handlers.each do |key, handler|
- pools << handler.connection_pool_list.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
+ pools.concat(handler.connection_pool_list.reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! })
end
- pools.flatten
+ pools
end
def self.complete(pools)
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index d5375390c7..f06a98bd98 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -2,6 +2,7 @@
require "active_record"
require "rails"
+require "active_support/core_ext/object/try"
require "active_model/railtie"
# For now, action_controller must always be present with
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index d17acc408c..d699e2e21b 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -2,6 +2,8 @@
require "active_record"
+databases = ActiveRecord::Tasks::DatabaseTasks.setup_initial_database_yaml
+
db_namespace = namespace :db do
desc "Set the environment value for the database"
task "environment:set" => :load_config do
@@ -23,7 +25,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Create #{spec_name} database for current environment"
task spec_name => :load_config do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -42,7 +44,7 @@ db_namespace = namespace :db do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Drop #{spec_name} database for current environment"
task spec_name => [:load_config, :check_protected_environments] do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -101,7 +103,7 @@ db_namespace = namespace :db do
end
namespace :migrate do
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Migrate #{spec_name} database for current environment"
task spec_name => :load_config do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -110,7 +112,7 @@ db_namespace = namespace :db do
end
end
- # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
+ desc "Rolls back the database one migration and re-migrates up (options: STEP=x, VERSION=x)."
task redo: :load_config do
raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
@@ -126,7 +128,7 @@ db_namespace = namespace :db do
# desc 'Resets your database using your migrations for the current environment'
task reset: ["db:drop", "db:create", "db:migrate"]
- # desc 'Runs the "up" for a given migration VERSION.'
+ desc 'Runs the "up" for a given migration VERSION.'
task up: :load_config do
ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:up")
@@ -142,7 +144,7 @@ db_namespace = namespace :db do
end
namespace :up do
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
task spec_name => :load_config do
raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
@@ -160,7 +162,7 @@ db_namespace = namespace :db do
end
end
- # desc 'Runs the "down" for a given migration VERSION.'
+ desc 'Runs the "down" for a given migration VERSION.'
task down: :load_config do
ActiveRecord::Tasks::DatabaseTasks.raise_for_multi_db(command: "db:migrate:down")
@@ -176,7 +178,7 @@ db_namespace = namespace :db do
end
namespace :down do
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
task spec_name => :load_config do
raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
@@ -203,7 +205,7 @@ db_namespace = namespace :db do
end
namespace :status do
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Display status of migrations for #{spec_name} database"
task spec_name => :load_config do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -228,7 +230,7 @@ db_namespace = namespace :db do
db_namespace["_dump"].invoke
end
- # desc 'Drops and recreates the database from db/schema.rb for the current environment and loads the seeds.'
+ desc "Drops and recreates the database from db/schema.rb for the current environment and loads the seeds."
task reset: [ "db:drop", "db:setup" ]
# desc "Retrieves the charset for the current environment's database"
@@ -263,10 +265,12 @@ db_namespace = namespace :db do
end
abort %{Run `rails db:migrate` to update your database then try again.}
end
+ ensure
+ ActiveRecord::Base.establish_connection(ActiveRecord::Tasks::DatabaseTasks.env.to_sym)
end
namespace :abort_if_pending_migrations do
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
# desc "Raises an error if there are pending migrations for #{spec_name} database"
task spec_name => :load_config do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
@@ -295,10 +299,11 @@ db_namespace = namespace :db do
ActiveRecord::Base.configurations.configs_for(env_name: ActiveRecord::Tasks::DatabaseTasks.env).each do |db_config|
ActiveRecord::Base.establish_connection(db_config.config)
- ActiveRecord::Tasks::DatabaseTasks.migrate
-
# Skipped when no database
- ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ if ActiveRecord::Base.dump_schema_after_migration
+ ActiveRecord::Tasks::DatabaseTasks.dump_schema(db_config.config, ActiveRecord::Base.schema_format, db_config.spec_name)
+ end
rescue ActiveRecord::NoDatabaseError
ActiveRecord::Tasks::DatabaseTasks.create_current(db_config.env_name, db_config.spec_name)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 0be9ba7d7b..0a14a33c1d 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -340,7 +340,7 @@ module ActiveRecord
}
relation = except(:group).distinct!(false)
- relation.group_values = group_aliases
+ relation.group_values = group_fields
relation.select_values = select_values
calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil) }
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 6deb9c7da8..1dbf4808fd 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -354,7 +354,7 @@ module ActiveRecord
conditions = sanitize_forbidden_attributes(conditions)
if distinct_value && offset_value
- relation = limit(1)
+ relation = except(:order).limit!(1)
else
relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 87a53966e4..2876bae302 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -138,7 +138,7 @@ module ActiveRecord
end
def includes!(*args) # :nodoc:
- args.reject!(&:blank?)
+ args.compact_blank!
args.flatten!
self.includes_values |= args
@@ -265,7 +265,7 @@ module ActiveRecord
end
def _select!(*fields) # :nodoc:
- fields.reject!(&:blank?)
+ fields.compact_blank!
fields.flatten!
self.select_values += fields
self
@@ -952,7 +952,7 @@ module ActiveRecord
def optimizer_hints!(*args) # :nodoc:
args.flatten!
- self.optimizer_hints_values += args
+ self.optimizer_hints_values |= args
self
end
@@ -965,7 +965,7 @@ module ActiveRecord
def reverse_order! # :nodoc:
orders = order_values.uniq
- orders.reject!(&:blank?)
+ orders.compact_blank!
self.order_values = reverse_sql_order(orders)
self
end
@@ -1053,7 +1053,7 @@ module ActiveRecord
)
arel.skip(Arel::Nodes::BindParam.new(offset_attribute))
end
- arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty?
+ arel.group(*arel_columns(group_values.uniq.compact_blank)) unless group_values.empty?
build_order(arel)
@@ -1095,26 +1095,44 @@ module ActiveRecord
end
def build_left_outer_joins(manager, outer_joins, aliases)
- buckets = { association_join: valid_association_list(outer_joins) }
+ buckets = Hash.new { |h, k| h[k] = [] }
+ buckets[:association_join] = valid_association_list(outer_joins)
build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
end
def build_joins(manager, joins, aliases)
+ buckets = Hash.new { |h, k| h[k] = [] }
+
unless left_outer_joins_values.empty?
left_joins = valid_association_list(left_outer_joins_values.flatten)
- joins.unshift construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
+ buckets[:stashed_join] << construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
end
- buckets = joins.group_by do |join|
+ joins.map! do |join|
+ if join.is_a?(String)
+ table.create_string_join(Arel.sql(join.strip)) unless join.blank?
+ else
+ join
+ end
+ end.compact_blank!.uniq!
+
+ while joins.first.is_a?(Arel::Nodes::Join)
+ join_node = joins.shift
+ if join_node.is_a?(Arel::Nodes::StringJoin) && !buckets[:stashed_join].empty?
+ buckets[:join_node] << join_node
+ else
+ buckets[:leading_join] << join_node
+ end
+ end
+
+ joins.each do |join|
case join
- when String
- :string_join
when Hash, Symbol, Array
- :association_join
+ buckets[:association_join] << join
when ActiveRecord::Associations::JoinDependency
- :stashed_join
+ buckets[:stashed_join] << join
when Arel::Nodes::Join
- :join_node
+ buckets[:join_node] << join
else
raise "unknown class: %s" % join.class.name
end
@@ -1124,25 +1142,21 @@ module ActiveRecord
end
def build_join_query(manager, buckets, join_type, aliases)
- buckets.default = []
-
association_joins = buckets[:association_join]
stashed_joins = buckets[:stashed_join]
- join_nodes = buckets[:join_node].tap(&:uniq!)
- string_joins = buckets[:string_join].delete_if(&:blank?).map!(&:strip).tap(&:uniq!)
-
- string_joins.map! { |join| table.create_string_join(Arel.sql(join)) }
+ leading_joins = buckets[:leading_join]
+ join_nodes = buckets[:join_node]
join_sources = manager.join_sources
- join_sources.concat(join_nodes) unless join_nodes.empty?
+ join_sources.concat(leading_joins) unless leading_joins.empty?
unless association_joins.empty? && stashed_joins.empty?
- alias_tracker = alias_tracker(join_nodes + string_joins, aliases)
+ alias_tracker = alias_tracker(leading_joins + join_nodes, aliases)
join_dependency = construct_join_dependency(association_joins, join_type)
join_sources.concat(join_dependency.join_constraints(stashed_joins, alias_tracker))
end
- join_sources.concat(string_joins) unless string_joins.empty?
+ join_sources.concat(join_nodes) unless join_nodes.empty?
end
def build_select(arel)
@@ -1227,7 +1241,7 @@ module ActiveRecord
def build_order(arel)
orders = order_values.uniq
- orders.reject!(&:blank?)
+ orders.compact_blank!
arel.order(*orders) unless orders.empty?
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 2f7cc07221..f4b1f536b3 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -146,7 +146,11 @@ HEADER
raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
next if column.name == pk
type, colspec = column_spec(column)
- tbl.print " t.#{type} #{column.name.inspect}"
+ if type.is_a?(Symbol)
+ tbl.print " t.#{type} #{column.name.inspect}"
+ else
+ tbl.print " t.column #{column.name.inspect}, #{type.inspect}"
+ end
tbl.print ", #{format_colspec(colspec)}" if colspec.present?
tbl.puts
end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index 741fea43ce..9fc62a99f8 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -11,7 +11,7 @@ module ActiveRecord #:nodoc:
end
def serializable_hash(options = nil)
- options = options.try(:dup) || {}
+ options = options ? options.dup : {}
options[:except] = Array(options[:except]).map(&:to_s)
options[:except] |= Array(self.class.inheritance_column)
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index aecc9350e8..300e67b0aa 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -4,7 +4,6 @@ require "active_record/database_configurations"
module ActiveRecord
module Tasks # :nodoc:
- class DatabaseAlreadyExists < StandardError; end # :nodoc:
class DatabaseNotSupported < StandardError; end # :nodoc:
# ActiveRecord::Tasks::DatabaseTasks is a utility class, which encapsulates
@@ -141,10 +140,21 @@ module ActiveRecord
end
end
- def for_each
+ def setup_initial_database_yaml
+ return {} unless defined?(Rails)
+
+ begin
+ Rails.application.config.load_database_yaml
+ rescue
+ $stderr.puts "Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB."
+
+ {}
+ end
+ end
+
+ def for_each(databases)
return {} unless defined?(Rails)
- databases = Rails.application.config.load_database_yaml
database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env)
# if this is a single database application we don't want tasks for each primary database
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 0d9917a4db..b9a8ccb22c 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -3,6 +3,8 @@
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
+ ER_DB_CREATE_EXISTS = 1007
+
delegate :connection, :establish_connection, to: ActiveRecord::Base
def initialize(configuration)
@@ -13,12 +15,6 @@ module ActiveRecord
establish_connection configuration_without_database
connection.create_database configuration["database"], creation_options
establish_connection configuration
- rescue ActiveRecord::StatementInvalid => error
- if error.message.include?("database exists")
- raise DatabaseAlreadyExists
- else
- raise
- end
end
def drop
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index 626ffdfdf9..fc37db216d 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -21,12 +21,6 @@ module ActiveRecord
connection.create_database configuration["database"],
configuration.merge("encoding" => encoding)
establish_connection configuration
- rescue ActiveRecord::StatementInvalid => error
- if error.cause.is_a?(PG::DuplicateDatabase)
- raise DatabaseAlreadyExists
- else
- raise
- end
end
def drop
diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb
index 8086102bde..0416ff58de 100644
--- a/activerecord/lib/arel/nodes/node.rb
+++ b/activerecord/lib/arel/nodes/node.rb
@@ -6,7 +6,6 @@ module Arel # :nodoc: all
# Abstract base class for all AST nodes
class Node
include Arel::FactoryMethods
- include Enumerable
###
# Factory method to create a Nodes::Not node that has the recipient of
@@ -38,13 +37,6 @@ module Arel # :nodoc: all
collector = engine.connection.visitor.accept self, collector
collector.value
end
-
- # Iterate through AST, nodes will be yielded depth-first
- def each(&block)
- return enum_for(:each) unless block_given?
-
- ::Arel::Visitors::DepthFirst.new(block).accept self
- end
end
end
end
diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb
index dece478615..895d394363 100644
--- a/activerecord/lib/arel/predications.rb
+++ b/activerecord/lib/arel/predications.rb
@@ -37,7 +37,7 @@ module Arel # :nodoc: all
def between(other)
if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1
self.in([])
- elsif open_ended?(other.begin)
+ elsif other.begin.nil? || open_ended?(other.begin)
if other.end.nil? || open_ended?(other.end)
not_in([])
elsif other.exclude_end?
@@ -85,7 +85,7 @@ Passing a range to `#in` is deprecated. Call `#between`, instead.
def not_between(other)
if unboundable?(other.begin) == 1 || unboundable?(other.end) == -1
not_in([])
- elsif open_ended?(other.begin)
+ elsif other.begin.nil? || open_ended?(other.begin)
if other.end.nil? || open_ended?(other.end)
self.in([])
elsif other.exclude_end?
diff --git a/activerecord/lib/arel/visitors.rb b/activerecord/lib/arel/visitors.rb
index e350f52e65..a1097f6750 100644
--- a/activerecord/lib/arel/visitors.rb
+++ b/activerecord/lib/arel/visitors.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "arel/visitors/visitor"
-require "arel/visitors/depth_first"
require "arel/visitors/to_sql"
require "arel/visitors/sqlite"
require "arel/visitors/postgresql"
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
deleted file mode 100644
index 98c3f92cf1..0000000000
--- a/activerecord/lib/arel/visitors/depth_first.rb
+++ /dev/null
@@ -1,203 +0,0 @@
-# frozen_string_literal: true
-
-module Arel # :nodoc: all
- module Visitors
- class DepthFirst < Arel::Visitors::Visitor
- def initialize(block = nil)
- @block = block || Proc.new
- super()
- end
-
- private
- def visit(o, _ = nil)
- super
- @block.call o
- end
-
- def unary(o)
- visit o.expr
- end
- alias :visit_Arel_Nodes_Else :unary
- alias :visit_Arel_Nodes_Group :unary
- alias :visit_Arel_Nodes_Cube :unary
- alias :visit_Arel_Nodes_RollUp :unary
- alias :visit_Arel_Nodes_GroupingSet :unary
- alias :visit_Arel_Nodes_GroupingElement :unary
- alias :visit_Arel_Nodes_Grouping :unary
- alias :visit_Arel_Nodes_Having :unary
- alias :visit_Arel_Nodes_Lateral :unary
- alias :visit_Arel_Nodes_Limit :unary
- alias :visit_Arel_Nodes_Not :unary
- alias :visit_Arel_Nodes_Offset :unary
- alias :visit_Arel_Nodes_On :unary
- alias :visit_Arel_Nodes_Ordering :unary
- alias :visit_Arel_Nodes_Ascending :unary
- alias :visit_Arel_Nodes_Descending :unary
- alias :visit_Arel_Nodes_UnqualifiedColumn :unary
- alias :visit_Arel_Nodes_OptimizerHints :unary
- alias :visit_Arel_Nodes_ValuesList :unary
-
- def function(o)
- visit o.expressions
- visit o.alias
- visit o.distinct
- end
- alias :visit_Arel_Nodes_Avg :function
- alias :visit_Arel_Nodes_Exists :function
- alias :visit_Arel_Nodes_Max :function
- alias :visit_Arel_Nodes_Min :function
- alias :visit_Arel_Nodes_Sum :function
-
- def visit_Arel_Nodes_NamedFunction(o)
- visit o.name
- visit o.expressions
- visit o.distinct
- visit o.alias
- end
-
- def visit_Arel_Nodes_Count(o)
- visit o.expressions
- visit o.alias
- visit o.distinct
- end
-
- def visit_Arel_Nodes_Case(o)
- visit o.case
- visit o.conditions
- visit o.default
- end
-
- def nary(o)
- o.children.each { |child| visit child }
- end
- alias :visit_Arel_Nodes_And :nary
-
- def binary(o)
- visit o.left
- visit o.right
- end
- alias :visit_Arel_Nodes_As :binary
- alias :visit_Arel_Nodes_Assignment :binary
- alias :visit_Arel_Nodes_Between :binary
- alias :visit_Arel_Nodes_Concat :binary
- alias :visit_Arel_Nodes_DeleteStatement :binary
- alias :visit_Arel_Nodes_DoesNotMatch :binary
- alias :visit_Arel_Nodes_Equality :binary
- alias :visit_Arel_Nodes_FullOuterJoin :binary
- alias :visit_Arel_Nodes_GreaterThan :binary
- alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
- alias :visit_Arel_Nodes_In :binary
- alias :visit_Arel_Nodes_InfixOperation :binary
- alias :visit_Arel_Nodes_JoinSource :binary
- alias :visit_Arel_Nodes_InnerJoin :binary
- alias :visit_Arel_Nodes_LessThan :binary
- alias :visit_Arel_Nodes_LessThanOrEqual :binary
- alias :visit_Arel_Nodes_Matches :binary
- alias :visit_Arel_Nodes_NotEqual :binary
- alias :visit_Arel_Nodes_NotIn :binary
- alias :visit_Arel_Nodes_NotRegexp :binary
- alias :visit_Arel_Nodes_IsNotDistinctFrom :binary
- alias :visit_Arel_Nodes_IsDistinctFrom :binary
- alias :visit_Arel_Nodes_Or :binary
- alias :visit_Arel_Nodes_OuterJoin :binary
- alias :visit_Arel_Nodes_Regexp :binary
- alias :visit_Arel_Nodes_RightOuterJoin :binary
- alias :visit_Arel_Nodes_TableAlias :binary
- alias :visit_Arel_Nodes_When :binary
-
- def visit_Arel_Nodes_StringJoin(o)
- visit o.left
- end
-
- def visit_Arel_Attribute(o)
- visit o.relation
- visit o.name
- end
- alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
- alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
- alias :visit_Arel_Attributes_String :visit_Arel_Attribute
- alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
- alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
- alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
- alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute
-
- def visit_Arel_Table(o)
- visit o.name
- end
-
- def terminal(o)
- end
- alias :visit_ActiveSupport_Multibyte_Chars :terminal
- alias :visit_ActiveSupport_StringInquirer :terminal
- alias :visit_Arel_Nodes_Lock :terminal
- alias :visit_Arel_Nodes_Node :terminal
- alias :visit_Arel_Nodes_SqlLiteral :terminal
- alias :visit_Arel_Nodes_BindParam :terminal
- alias :visit_Arel_Nodes_Window :terminal
- alias :visit_Arel_Nodes_True :terminal
- alias :visit_Arel_Nodes_False :terminal
- alias :visit_BigDecimal :terminal
- alias :visit_Class :terminal
- alias :visit_Date :terminal
- alias :visit_DateTime :terminal
- alias :visit_FalseClass :terminal
- alias :visit_Float :terminal
- alias :visit_Integer :terminal
- alias :visit_NilClass :terminal
- alias :visit_String :terminal
- alias :visit_Symbol :terminal
- alias :visit_Time :terminal
- alias :visit_TrueClass :terminal
-
- def visit_Arel_Nodes_InsertStatement(o)
- visit o.relation
- visit o.columns
- visit o.values
- end
-
- def visit_Arel_Nodes_SelectCore(o)
- visit o.projections
- visit o.source
- visit o.wheres
- visit o.groups
- visit o.windows
- visit o.havings
- end
-
- def visit_Arel_Nodes_SelectStatement(o)
- visit o.cores
- visit o.orders
- visit o.limit
- visit o.lock
- visit o.offset
- end
-
- def visit_Arel_Nodes_UpdateStatement(o)
- visit o.relation
- visit o.values
- visit o.wheres
- visit o.orders
- visit o.limit
- end
-
- def visit_Arel_Nodes_Comment(o)
- visit o.values
- end
-
- def visit_Array(o)
- o.each { |i| visit i }
- end
- alias :visit_Set :visit_Array
-
- def visit_Hash(o)
- o.each { |k, v| visit(k); visit(v) }
- end
-
- DISPATCH = dispatch_cache
-
- def get_dispatch_cache
- DISPATCH
- end
- end
- end
-end
diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb
index dd77cfdf66..6cb866715f 100644
--- a/activerecord/lib/arel/visitors/mysql.rb
+++ b/activerecord/lib/arel/visitors/mysql.rb
@@ -48,6 +48,14 @@ module Arel # :nodoc: all
visit_Arel_Nodes_IsNotDistinctFrom o, collector
end
+ def visit_Arel_Nodes_Regexp(o, collector)
+ infix_value o, collector, " REGEXP "
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ infix_value o, collector, " NOT REGEXP "
+ end
+
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
# these, we must use a subquery.
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
index aab66301ef..a0a74d365c 100644
--- a/activerecord/lib/arel/visitors/oracle.rb
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -9,7 +9,7 @@ module Arel # :nodoc: all
# if need to select first records without ORDER BY and GROUP BY and without DISTINCT
# then can use simple ROWNUM in WHERE clause
- if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/
+ if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && !o.cores.first.set_quantifier.class.to_s.match?(/Distinct/)
o.cores.last.wheres.push Nodes::LessThanOrEqual.new(
Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr
)
@@ -122,7 +122,7 @@ module Arel # :nodoc: all
o.orders = []
orders.each_with_index do |order, i|
o.orders <<
- Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}")
+ Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i.match?(order)}")
end
o
end
diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb
index d65ac820bc..9066307aed 100644
--- a/activerecord/lib/arel/visitors/visitor.rb
+++ b/activerecord/lib/arel/visitors/visitor.rb
@@ -15,7 +15,7 @@ module Arel # :nodoc: all
attr_reader :dispatch
def self.dispatch_cache
- Hash.new do |hash, klass|
+ @dispatch_cache ||= Hash.new do |hash, klass|
hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/annotate_test.rb b/activerecord/test/cases/adapters/mysql2/annotate_test.rb
deleted file mode 100644
index b512540073..0000000000
--- a/activerecord/test/cases/adapters/mysql2/annotate_test.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require "cases/helper"
-require "models/post"
-
-class Mysql2AnnotateTest < ActiveRecord::Mysql2TestCase
- fixtures :posts
-
- def test_annotate_wraps_content_in_an_inline_comment
- assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
- posts = Post.select(:id).annotate("foo")
- assert posts.first
- end
- end
-
- def test_annotate_is_sanitized
- assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
- posts = Post.select(:id).annotate("*/foo/*")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
- posts = Post.select(:id).annotate("**//foo//**")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/ /\* bar \*/}) do
- posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
- posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
- assert posts.first
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb
index 832f5d61d1..1168b3677e 100644
--- a/activerecord/test/cases/adapters/mysql2/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb
@@ -1,11 +1,20 @@
# frozen_string_literal: true
require "cases/helper"
+require "support/schema_dumping_helper"
class Mysql2EnumTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+
class EnumTest < ActiveRecord::Base
end
+ def setup
+ EnumTest.connection.create_table :enum_tests, id: false, force: true do |t|
+ t.column :enum_column, "enum('text','blob','tiny','medium','long','unsigned','bigint')"
+ end
+ end
+
def test_enum_limit
column = EnumTest.columns_hash["enum_column"]
assert_equal 8, column.limit
@@ -20,4 +29,9 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase
column = EnumTest.columns_hash["enum_column"]
assert_not_predicate column, :bigint?
end
+
+ def test_schema_dumping
+ schema = dump_table_schema "enum_tests"
+ assert_match %r{t\.column "enum_column", "enum\('text','blob','tiny','medium','long','unsigned','bigint'\)"$}, schema
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
index e1f7a0b7c5..cfc1823773 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -20,6 +20,18 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
end
end
+ def test_database_exists_returns_false_if_database_does_not_exist
+ config = ActiveRecord::Base.configurations["arunit"].merge(database: "inexistent_activerecord_unittest")
+ assert_not ActiveRecord::ConnectionAdapters::Mysql2Adapter.database_exists?(config),
+ "expected database to not exist"
+ end
+
+ def test_database_exists_returns_true_when_the_database_exists
+ config = ActiveRecord::Base.configurations["arunit"]
+ assert ActiveRecord::ConnectionAdapters::Mysql2Adapter.database_exists?(config),
+ "expected database #{config[:database]} to exist"
+ end
+
def test_columns_for_distinct_zero_orders
assert_equal "posts.id",
@conn.columns_for_distinct("posts.id", [])
@@ -213,6 +225,21 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
end
end
+ def test_read_timeout_exception
+ ActiveRecord::Base.establish_connection(
+ ActiveRecord::Base.configurations[:arunit].merge("read_timeout" => 1)
+ )
+
+ error = assert_raises(ActiveRecord::AdapterTimeout) do
+ ActiveRecord::Base.connection.execute("SELECT SLEEP(2)")
+ end
+ assert_kind_of ActiveRecord::QueryAborted, error
+
+ assert_equal Mysql2::Error::TimeoutError, error.cause.class
+ ensure
+ ActiveRecord::Base.establish_connection :arunit
+ end
+
private
def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block)
super(@conn, "ex", definition, &block)
diff --git a/activerecord/test/cases/adapters/mysql2/set_test.rb b/activerecord/test/cases/adapters/mysql2/set_test.rb
new file mode 100644
index 0000000000..89107e142f
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/set_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+class Mysql2SetTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+
+ class SetTest < ActiveRecord::Base
+ end
+
+ def setup
+ SetTest.connection.create_table :set_tests, id: false, force: true do |t|
+ t.column :set_column, "set('text','blob','tiny','medium','long','unsigned','bigint')"
+ end
+ end
+
+ def test_should_not_be_unsigned
+ column = SetTest.columns_hash["set_column"]
+ assert_not_predicate column, :unsigned?
+ end
+
+ def test_should_not_be_bigint
+ column = SetTest.columns_hash["set_column"]
+ assert_not_predicate column, :bigint?
+ end
+
+ def test_schema_dumping
+ schema = dump_table_schema "set_tests"
+ assert_match %r{t\.column "set_column", "set\('text','blob','tiny','medium','long','unsigned','bigint'\)"$}, schema
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
index 52e283f247..2041cc308f 100644
--- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
@@ -92,7 +92,7 @@ module ActiveRecord
test "raises StatementTimeout when statement timeout exceeded" do
skip unless ActiveRecord::Base.connection.show_variable("max_execution_time")
- assert_raises(ActiveRecord::StatementTimeout) do
+ error = assert_raises(ActiveRecord::StatementTimeout) do
s = Sample.create!(value: 1)
latch1 = Concurrent::CountDownLatch.new
latch2 = Concurrent::CountDownLatch.new
@@ -117,10 +117,11 @@ module ActiveRecord
thread.join
end
end
+ assert_kind_of ActiveRecord::QueryAborted, error
end
test "raises QueryCanceled when canceling statement due to user request" do
- assert_raises(ActiveRecord::QueryCanceled) do
+ error = assert_raises(ActiveRecord::QueryCanceled) do
s = Sample.create!(value: 1)
latch = Concurrent::CountDownLatch.new
@@ -144,6 +145,7 @@ module ActiveRecord
thread.join
end
end
+ assert_kind_of ActiveRecord::QueryAborted, error
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/annotate_test.rb b/activerecord/test/cases/adapters/postgresql/annotate_test.rb
deleted file mode 100644
index 42a2861511..0000000000
--- a/activerecord/test/cases/adapters/postgresql/annotate_test.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require "cases/helper"
-require "models/post"
-
-class PostgresqlAnnotateTest < ActiveRecord::PostgreSQLTestCase
- fixtures :posts
-
- def test_annotate_wraps_content_in_an_inline_comment
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
- posts = Post.select(:id).annotate("foo")
- assert posts.first
- end
- end
-
- def test_annotate_is_sanitized
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
- posts = Post.select(:id).annotate("*/foo/*")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
- posts = Post.select(:id).annotate("**//foo//**")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/ /\* bar \*/}) do
- posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
- posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
- assert posts.first
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index 1aa0348879..ff2ab22a80 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -54,8 +54,12 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
type = PostgresqlMoney.type_for_attribute("wealth")
assert_equal(12345678.12, type.cast(+"$12,345,678.12"))
assert_equal(12345678.12, type.cast(+"$12.345.678,12"))
+ assert_equal(12345678.12, type.cast(+"12,345,678.12"))
+ assert_equal(12345678.12, type.cast(+"12.345.678,12"))
assert_equal(-1.15, type.cast(+"-$1.15"))
assert_equal(-2.25, type.cast(+"($2.25)"))
+ assert_equal(-1.15, type.cast(+"-1.15"))
+ assert_equal(-2.25, type.cast(+"(2.25)"))
end
def test_schema_dumping
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 68bc87eaf8..830c0892d3 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -24,6 +24,18 @@ module ActiveRecord
end
end
+ def test_database_exists_returns_false_when_the_database_does_not_exist
+ config = { database: "non_extant_database", adapter: "postgresql" }
+ assert_not ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.database_exists?(config),
+ "expected database #{config[:database]} to not exist"
+ end
+
+ def test_database_exists_returns_true_when_the_database_exists
+ config = ActiveRecord::Base.configurations["arunit"]
+ assert ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.database_exists?(config),
+ "expected database #{config[:database]} to exist"
+ end
+
def test_primary_key
with_example_table do
assert_equal "id", @connection.primary_key("ex")
@@ -241,9 +253,11 @@ module ActiveRecord
def test_expression_index
with_example_table do
- @connection.add_index "ex", "mod(id, 10), abs(number)", name: "expression"
+ expr = "mod(id, 10), abs(number)"
+ @connection.add_index "ex", expr, name: "expression"
index = @connection.indexes("ex").find { |idx| idx.name == "expression" }
- assert_equal "mod(id, 10), abs(number)", index.columns
+ assert_equal expr, index.columns
+ assert_equal true, @connection.index_exists?("ex", expr, name: "expression")
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/annotate_test.rb b/activerecord/test/cases/adapters/sqlite3/annotate_test.rb
deleted file mode 100644
index 6567a5eca3..0000000000
--- a/activerecord/test/cases/adapters/sqlite3/annotate_test.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-# frozen_string_literal: true
-
-require "cases/helper"
-require "models/post"
-
-class SQLite3AnnotateTest < ActiveRecord::SQLite3TestCase
- fixtures :posts
-
- def test_annotate_wraps_content_in_an_inline_comment
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
- posts = Post.select(:id).annotate("foo")
- assert posts.first
- end
- end
-
- def test_annotate_is_sanitized
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
- posts = Post.select(:id).annotate("*/foo/*")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
- posts = Post.select(:id).annotate("**//foo//**")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/ /\* bar \*/}) do
- posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
- assert posts.first
- end
-
- assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
- posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
- assert posts.first
- end
- end
-end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 508f7d8945..b6d72c7bcd 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -30,6 +30,17 @@ module ActiveRecord
end
end
+ def test_database_exists_returns_false_when_the_database_does_not_exist
+ assert_not SQLite3Adapter.database_exists?(adapter: "sqlite3", database: "non_extant_db"),
+ "expected non_extant_db to not exist"
+ end
+
+ def test_database_exists_returns_true_when_databae_exists
+ config = ActiveRecord::Base.configurations["arunit"]
+ assert SQLite3Adapter.database_exists?(config),
+ "expected #{config[:database]} to exist"
+ end
+
unless in_memory_db?
def test_connect_with_url
original_connection = ActiveRecord::Base.remove_connection
@@ -53,6 +64,11 @@ module ActiveRecord
end
end
+ def test_database_exists_returns_true_for_an_in_memory_db
+ assert SQLite3Adapter.database_exists?(database: ":memory:"),
+ "Expected in memory database to exist"
+ end
+
def test_column_types
owner = Owner.create!(name: "hello".encode("ascii-8bit"))
owner.reload
diff --git a/activerecord/test/cases/annotate_test.rb b/activerecord/test/cases/annotate_test.rb
new file mode 100644
index 0000000000..4d71d28f83
--- /dev/null
+++ b/activerecord/test/cases/annotate_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+class AnnotateTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_annotate_wraps_content_in_an_inline_comment
+ quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts")
+
+ assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
+ posts = Post.select(:id).annotate("foo")
+ assert posts.first
+ end
+ end
+
+ def test_annotate_is_sanitized
+ quoted_posts_id, quoted_posts = regexp_escape_table_name("posts.id"), regexp_escape_table_name("posts")
+
+ assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
+ posts = Post.select(:id).annotate("*/foo/*")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/}i) do
+ posts = Post.select(:id).annotate("**//foo//**")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* foo \*/ /\* bar \*/}i) do
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT #{quoted_posts_id} FROM #{quoted_posts} /\* \+ MAX_EXECUTION_TIME\(1\) \*/}i) do
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
+ assert posts.first
+ end
+ end
+
+ private
+ def regexp_escape_table_name(name)
+ Regexp.escape(Post.connection.quote_table_name(name))
+ end
+end
diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb
index c7bd0a053b..7ebb90c6fd 100644
--- a/activerecord/test/cases/arel/attributes/attribute_test.rb
+++ b/activerecord/test/cases/arel/attributes/attribute_test.rb
@@ -638,6 +638,18 @@ module Arel
)
end
+ if Gem::Version.new("2.7.0") <= Gem::Version.new(RUBY_VERSION)
+ it "can be constructed with a range implicitly starting at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(eval("..0")) # eval for backwards compatibility
+
+ node.must_equal Nodes::LessThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+ end
+
if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
it "can be constructed with a range implicitly ending at Infinity" do
attribute = Attribute.new nil, nil
@@ -839,6 +851,18 @@ module Arel
)
end
+ if Gem::Version.new("2.7.0") <= Gem::Version.new(RUBY_VERSION)
+ it "can be constructed with a range implicitly starting at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(eval("..0")) # eval for backwards compatibility
+
+ node.must_equal Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+ end
+
if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
it "can be constructed with a range implicitly ending at Infinity" do
attribute = Attribute.new nil, nil
diff --git a/activerecord/test/cases/arel/nodes/node_test.rb b/activerecord/test/cases/arel/nodes/node_test.rb
index f4f07ef2c5..8a9ecd84ca 100644
--- a/activerecord/test/cases/arel/nodes/node_test.rb
+++ b/activerecord/test/cases/arel/nodes/node_test.rb
@@ -14,28 +14,9 @@ module Arel
}.grep(Class).each do |klass|
next if Nodes::SqlLiteral == klass
next if Nodes::BindParam == klass
- next if klass.name =~ /^Arel::Nodes::(?:Test|.*Test$)/
+ next if /^Arel::Nodes::(?:Test|.*Test$)/.match?(klass.name)
assert klass.ancestors.include?(Nodes::Node), klass.name
end
end
-
- def test_each
- list = []
- node = Nodes::Node.new
- node.each { |n| list << n }
- assert_equal [node], list
- end
-
- def test_generator
- list = []
- node = Nodes::Node.new
- node.each.each { |n| list << n }
- assert_equal [node], list
- end
-
- def test_enumerable
- node = Nodes::Node.new
- assert_kind_of Enumerable, node
- end
end
end
diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb
index e6c49cd429..4512d8e8a6 100644
--- a/activerecord/test/cases/arel/select_manager_test.rb
+++ b/activerecord/test/cases/arel/select_manager_test.rb
@@ -369,16 +369,6 @@ module Arel
mgr = table.from
assert mgr.ast
end
-
- it "should allow orders to work when the ast is grepped" do
- table = Table.new :users
- mgr = table.from
- mgr.project Arel.sql "*"
- mgr.from table
- mgr.orders << Arel::Nodes::Ascending.new(Arel.sql("foo"))
- mgr.ast.grep(Arel::Nodes::OuterJoin)
- mgr.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo ASC }
- end
end
describe "taken" do
@@ -524,7 +514,7 @@ module Arel
assert_equal "bar", join.right
end
- it "should create join nodes with a outer join klass" do
+ it "should create join nodes with an outer join klass" do
relation = Arel::SelectManager.new
join = relation.create_join "foo", "bar", Arel::Nodes::OuterJoin
assert_kind_of Arel::Nodes::OuterJoin, join
diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb
deleted file mode 100644
index 106be2311d..0000000000
--- a/activerecord/test/cases/arel/visitors/depth_first_test.rb
+++ /dev/null
@@ -1,276 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../helper"
-
-module Arel
- module Visitors
- class TestDepthFirst < Arel::Test
- Collector = Struct.new(:calls) do
- def call(object)
- calls << object
- end
- end
-
- def setup
- @collector = Collector.new []
- @visitor = Visitors::DepthFirst.new @collector
- end
-
- def test_raises_with_object
- assert_raises(TypeError) do
- @visitor.accept(Object.new)
- end
- end
-
-
- # unary ops
- [
- Arel::Nodes::Not,
- Arel::Nodes::Group,
- Arel::Nodes::On,
- Arel::Nodes::Grouping,
- Arel::Nodes::Offset,
- Arel::Nodes::Ordering,
- Arel::Nodes::StringJoin,
- Arel::Nodes::UnqualifiedColumn,
- Arel::Nodes::ValuesList,
- Arel::Nodes::Limit,
- Arel::Nodes::Else,
- ].each do |klass|
- define_method("test_#{klass.name.gsub('::', '_')}") do
- op = klass.new(:a)
- @visitor.accept op
- assert_equal [:a, op], @collector.calls
- end
- end
-
- # functions
- [
- Arel::Nodes::Exists,
- Arel::Nodes::Avg,
- Arel::Nodes::Min,
- Arel::Nodes::Max,
- Arel::Nodes::Sum,
- ].each do |klass|
- define_method("test_#{klass.name.gsub('::', '_')}") do
- func = klass.new(:a, "b")
- @visitor.accept func
- assert_equal [:a, "b", false, func], @collector.calls
- end
- end
-
- def test_named_function
- func = Arel::Nodes::NamedFunction.new(:a, :b, "c")
- @visitor.accept func
- assert_equal [:a, :b, false, "c", func], @collector.calls
- end
-
- def test_lock
- lock = Nodes::Lock.new true
- @visitor.accept lock
- assert_equal [lock], @collector.calls
- end
-
- def test_count
- count = Nodes::Count.new :a, :b, "c"
- @visitor.accept count
- assert_equal [:a, "c", :b, count], @collector.calls
- end
-
- def test_inner_join
- join = Nodes::InnerJoin.new :a, :b
- @visitor.accept join
- assert_equal [:a, :b, join], @collector.calls
- end
-
- def test_full_outer_join
- join = Nodes::FullOuterJoin.new :a, :b
- @visitor.accept join
- assert_equal [:a, :b, join], @collector.calls
- end
-
- def test_outer_join
- join = Nodes::OuterJoin.new :a, :b
- @visitor.accept join
- assert_equal [:a, :b, join], @collector.calls
- end
-
- def test_right_outer_join
- join = Nodes::RightOuterJoin.new :a, :b
- @visitor.accept join
- assert_equal [:a, :b, join], @collector.calls
- end
-
- def test_comment
- comment = Nodes::Comment.new ["foo"]
- @visitor.accept comment
- assert_equal ["foo", ["foo"], comment], @collector.calls
- end
-
- [
- Arel::Nodes::Assignment,
- Arel::Nodes::Between,
- Arel::Nodes::Concat,
- Arel::Nodes::DoesNotMatch,
- Arel::Nodes::Equality,
- Arel::Nodes::GreaterThan,
- Arel::Nodes::GreaterThanOrEqual,
- Arel::Nodes::In,
- Arel::Nodes::LessThan,
- Arel::Nodes::LessThanOrEqual,
- Arel::Nodes::Matches,
- Arel::Nodes::NotEqual,
- Arel::Nodes::NotIn,
- Arel::Nodes::Or,
- Arel::Nodes::TableAlias,
- Arel::Nodes::As,
- Arel::Nodes::DeleteStatement,
- Arel::Nodes::JoinSource,
- Arel::Nodes::When,
- ].each do |klass|
- define_method("test_#{klass.name.gsub('::', '_')}") do
- binary = klass.new(:a, :b)
- @visitor.accept binary
- assert_equal [:a, :b, binary], @collector.calls
- end
- end
-
- def test_Arel_Nodes_InfixOperation
- binary = Arel::Nodes::InfixOperation.new(:o, :a, :b)
- @visitor.accept binary
- assert_equal [:a, :b, binary], @collector.calls
- end
-
- # N-ary
- [
- Arel::Nodes::And,
- ].each do |klass|
- define_method("test_#{klass.name.gsub('::', '_')}") do
- binary = klass.new([:a, :b, :c])
- @visitor.accept binary
- assert_equal [:a, :b, :c, binary], @collector.calls
- end
- end
-
- [
- Arel::Attributes::Integer,
- Arel::Attributes::Float,
- Arel::Attributes::String,
- Arel::Attributes::Time,
- Arel::Attributes::Boolean,
- Arel::Attributes::Attribute
- ].each do |klass|
- define_method("test_#{klass.name.gsub('::', '_')}") do
- binary = klass.new(:a, :b)
- @visitor.accept binary
- assert_equal [:a, :b, binary], @collector.calls
- end
- end
-
- def test_table
- relation = Arel::Table.new(:users)
- @visitor.accept relation
- assert_equal ["users", relation], @collector.calls
- end
-
- def test_array
- node = Nodes::Or.new(:a, :b)
- list = [node]
- @visitor.accept list
- assert_equal [:a, :b, node, list], @collector.calls
- end
-
- def test_set
- node = Nodes::Or.new(:a, :b)
- set = Set.new([node])
- @visitor.accept set
- assert_equal [:a, :b, node, set], @collector.calls
- end
-
- def test_hash
- node = Nodes::Or.new(:a, :b)
- hash = { node => node }
- @visitor.accept hash
- assert_equal [:a, :b, node, :a, :b, node, hash], @collector.calls
- end
-
- def test_update_statement
- stmt = Nodes::UpdateStatement.new
- stmt.relation = :a
- stmt.values << :b
- stmt.wheres << :c
- stmt.orders << :d
- stmt.limit = :e
-
- @visitor.accept stmt
- assert_equal [:a, :b, stmt.values, :c, stmt.wheres, :d, stmt.orders,
- :e, stmt], @collector.calls
- end
-
- def test_select_core
- core = Nodes::SelectCore.new
- core.projections << :a
- core.froms = :b
- core.wheres << :c
- core.groups << :d
- core.windows << :e
- core.havings << :f
-
- @visitor.accept core
- assert_equal [
- :a, core.projections,
- :b, [],
- core.source,
- :c, core.wheres,
- :d, core.groups,
- :e, core.windows,
- :f, core.havings,
- core], @collector.calls
- end
-
- def test_select_statement
- ss = Nodes::SelectStatement.new
- ss.cores.replace [:a]
- ss.orders << :b
- ss.limit = :c
- ss.lock = :d
- ss.offset = :e
-
- @visitor.accept ss
- assert_equal [
- :a, ss.cores,
- :b, ss.orders,
- :c,
- :d,
- :e,
- ss], @collector.calls
- end
-
- def test_insert_statement
- stmt = Nodes::InsertStatement.new
- stmt.relation = :a
- stmt.columns << :b
- stmt.values = :c
-
- @visitor.accept stmt
- assert_equal [:a, :b, stmt.columns, :c, stmt], @collector.calls
- end
-
- def test_case
- node = Arel::Nodes::Case.new
- node.case = :a
- node.conditions << :b
- node.default = :c
-
- @visitor.accept node
- assert_equal [:a, :b, node.conditions, :c, node], @collector.calls
- end
-
- def test_node
- node = Nodes::Node.new
- @visitor.accept node
- assert_equal [node], @collector.calls
- end
- end
- end
-end
diff --git a/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
index a07a1a050a..36f9eb49a2 100644
--- a/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
+++ b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
@@ -48,7 +48,13 @@ module Arel
node = Nodes::Union.new(Nodes::True.new, Nodes::False.new)
assert_equal "( TRUE UNION FALSE )", node.to_sql
- node.first # from Nodes::Node's Enumerable mixin
+ visitor = Class.new(Visitor) {
+ def visit_Arel_Nodes_Union(o); end
+ alias :visit_Arel_Nodes_True :visit_Arel_Nodes_Union
+ alias :visit_Arel_Nodes_False :visit_Arel_Nodes_Union
+ }.new
+
+ visitor.accept(node)
assert_equal "( TRUE UNION FALSE )", node.to_sql
end
diff --git a/activerecord/test/cases/arel/visitors/mysql_test.rb b/activerecord/test/cases/arel/visitors/mysql_test.rb
index 5f37587957..05dccd126e 100644
--- a/activerecord/test/cases/arel/visitors/mysql_test.rb
+++ b/activerecord/test/cases/arel/visitors/mysql_test.rb
@@ -104,6 +104,52 @@ module Arel
sql.must_be_like %{ NOT "users"."name" <=> NULL }
end
end
+
+ describe "Nodes::Regexp" do
+ before do
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ it "should know how to visit" do
+ node = @table[:name].matches_regexp("foo.*")
+ node.must_be_kind_of Nodes::Regexp
+ compile(node).must_be_like %{
+ "users"."name" REGEXP 'foo.*'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches_regexp("foo.*"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" REGEXP 'foo.*')
+ }
+ end
+ end
+
+ describe "Nodes::NotRegexp" do
+ before do
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ it "should know how to visit" do
+ node = @table[:name].does_not_match_regexp("foo.*")
+ node.must_be_kind_of Nodes::NotRegexp
+ compile(node).must_be_like %{
+ "users"."name" NOT REGEXP 'foo.*'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match_regexp("foo.*"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT REGEXP 'foo.*')
+ }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 3525fa2ab8..6bd305306f 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -60,11 +60,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_does_not_use_order_by
- ActiveRecord::SQLCounter.clear_log
- Client.find(3).firm
- ensure
- sql_log = ActiveRecord::SQLCounter.log
- assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
+ sql_log = capture_sql { Client.find(3).firm }
+ assert sql_log.all? { |sql| !/order by/i.match?(sql) }, "ORDER BY was used in the query: #{sql_log}"
end
def test_belongs_to_with_primary_key
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 25cfa0a723..de9742b250 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
@@ -700,10 +700,17 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal ["id"], developers(:david).projects.select(:id).first.attributes.keys
end
+ def test_join_middle_table_alias
+ assert_equal(
+ 2,
+ Project.includes(:developers_projects).where.not("developers_projects.joined_on": nil).to_a.size
+ )
+ end
+
def test_join_table_alias
assert_equal(
3,
- Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).to_a.size
+ Developer.includes(projects: :developers).where.not("developers_projects_projects_join.joined_on": nil).to_a.size
)
end
@@ -716,7 +723,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal(
3,
- Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).group(group.join(",")).to_a.size
+ Developer.includes(projects: :developers).where.not("developers_projects_projects_join.joined_on": nil).group(group.join(",")).to_a.size
)
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 3ef25c7027..6c2a09c296 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -37,11 +37,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_does_not_use_order_by
- ActiveRecord::SQLCounter.clear_log
- companies(:first_firm).account
- ensure
- sql_log = ActiveRecord::SQLCounter.log
- assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
+ sql_log = capture_sql { companies(:first_firm).account }
+ assert sql_log.all? { |sql| !/order by/i.match?(sql) }, "ORDER BY was used in the query: #{sql_log}"
end
def test_has_one_cache_nils
@@ -712,6 +709,24 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
}
end
+ def test_polymorphic_has_one_with_touch_option_on_create_wont_cache_association_so_fetching_after_transaction_commit_works
+ assert_queries(4) {
+ chef = Chef.create(employable: DrinkDesignerWithPolymorphicTouchChef.new)
+ employable = chef.employable
+
+ assert_equal chef, employable.chef
+ }
+ end
+
+ def test_polymorphic_has_one_with_touch_option_on_update_will_touch_record_by_fetching_from_database_if_needed
+ DrinkDesignerWithPolymorphicTouchChef.create(chef: Chef.new)
+ designer = DrinkDesignerWithPolymorphicTouchChef.last
+
+ assert_queries(3) {
+ designer.update(name: "foo")
+ }
+ end
+
def test_has_one_with_touch_option_on_update
new_club = Club.create(name: "1000 Oaks")
new_club.create_membership
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index e0dac01f4a..166a59ec7b 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -13,7 +13,7 @@ require "models/tag"
class InnerJoinAssociationTest < ActiveRecord::TestCase
fixtures :authors, :author_addresses, :essays, :posts, :comments, :categories, :categories_posts, :categorizations,
- :taggings, :tags
+ :taggings, :tags, :people
def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
result = Author.joins(:thinking_posts, :welcome_posts).to_a
@@ -36,16 +36,47 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
end
def test_construct_finder_sql_does_not_table_name_collide_with_string_joins
- sql = Person.joins(:agents).joins("JOIN people agents_people ON agents_people.primary_contact_id = people.id").to_sql
- assert_match(/agents_people_2/i, sql)
+ string_join = <<~SQL
+ JOIN people agents_people ON agents_people.primary_contact_id = agents_people_2.id AND agents_people.id > agents_people_2.id
+ SQL
+
+ expected = people(:susan)
+ assert_sql(/agents_people_2/i) do
+ assert_equal [expected], Person.joins(:agents).joins(string_join)
+ end
end
def test_construct_finder_sql_does_not_table_name_collide_with_aliased_joins
- people = Person.arel_table
- agents = people.alias("agents_people")
- constraint = agents[:primary_contact_id].eq(people[:id])
- sql = Person.joins(:agents).joins(agents.create_join(agents, agents.create_on(constraint))).to_sql
- assert_match(/agents_people_2/i, sql)
+ agents = Person.arel_table.alias("agents_people")
+ agents_2 = Person.arel_table.alias("agents_people_2")
+ constraint = agents[:primary_contact_id].eq(agents_2[:id]).and(agents[:id].gt(agents_2[:id]))
+
+ expected = people(:susan)
+ assert_sql(/agents_people_2/i) do
+ assert_equal [expected], Person.joins(:agents).joins(agents.create_join(agents, agents.create_on(constraint)))
+ end
+ end
+
+ def test_user_supplied_joins_order_should_be_preserved
+ string_join = <<~SQL
+ JOIN people agents_people_2 ON agents_people_2.primary_contact_id = people.id
+ SQL
+ agents = Person.arel_table.alias("agents_people")
+ agents_2 = Person.arel_table.alias("agents_people_2")
+ constraint = agents[:primary_contact_id].eq(agents_2[:id]).and(agents[:id].gt(agents_2[:id]))
+
+ expected = people(:susan)
+ assert_equal [expected], Person.joins(string_join).joins(agents.create_join(agents, agents.create_on(constraint)))
+ end
+
+ def test_deduplicate_joins
+ posts = Post.arel_table
+ constraint = posts[:author_id].eq(Author.arel_attribute(:id))
+
+ authors = Author.joins(posts.create_join(posts, posts.create_on(constraint)))
+ authors = authors.joins(:author_address).merge(authors.where("posts.type": "SpecialPost"))
+
+ assert_equal [authors(:david)], authors
end
def test_construct_finder_sql_ignores_empty_joins_hash
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 7e61ac9d8b..3528ac045f 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -2,6 +2,7 @@
require "cases/helper"
require "models/author"
+require "models/book"
require "models/bird"
require "models/post"
require "models/comment"
@@ -12,12 +13,14 @@ require "models/developer"
require "models/computer"
require "models/invoice"
require "models/line_item"
+require "models/mouse"
require "models/order"
require "models/parrot"
require "models/pirate"
require "models/project"
require "models/ship"
require "models/ship_part"
+require "models/squeak"
require "models/tag"
require "models/tagging"
require "models/treasure"
@@ -385,6 +388,20 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
assert_predicate auditlog, :valid?
end
+
+ def test_validation_does_not_validate_non_dirty_association_target
+ mouse = Mouse.create!(name: "Will")
+ Squeak.create!(mouse: mouse)
+
+ mouse.name = nil
+ mouse.save! validate: false
+
+ squeak = Squeak.last
+
+ assert_equal true, squeak.valid?
+ assert_equal true, squeak.mouse.present?
+ assert_equal true, squeak.valid?
+ end
end
class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
@@ -1671,6 +1688,10 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te
super
@pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
@pirate.birds.create(name: "cookoo")
+
+ @author = Author.new(name: "DHH")
+ @author.published_books.build(name: "Rework", isbn: "1234")
+ @author.published_books.build(name: "Remote", isbn: "1234")
end
test "should automatically validate associations" do
@@ -1679,6 +1700,42 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te
assert_not_predicate @pirate, :valid?
end
+
+ test "rollbacks whole transaction and raises ActiveRecord::RecordInvalid when associations fail to #save! due to uniqueness validation failure" do
+ author_count_before_save = Author.count
+ book_count_before_save = Book.count
+
+ assert_no_difference "Author.count" do
+ assert_no_difference "Book.count" do
+ exception = assert_raises(ActiveRecord::RecordInvalid) do
+ @author.save!
+ end
+
+ assert_equal("Validation failed: Published books is invalid", exception.message)
+ end
+ end
+
+ assert_equal(author_count_before_save, Author.count)
+ assert_equal(book_count_before_save, Book.count)
+ end
+
+ test "rollbacks whole transaction when associations fail to #save due to uniqueness validation failure" do
+ author_count_before_save = Author.count
+ book_count_before_save = Book.count
+
+ assert_no_difference "Author.count" do
+ assert_no_difference "Book.count" do
+ assert_nothing_raised do
+ result = @author.save
+
+ assert_not(result)
+ end
+ end
+ end
+
+ assert_equal(author_count_before_save, Author.count)
+ assert_equal(book_count_before_save, Book.count)
+ end
end
class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index cf6e280898..0d0bf39f79 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -146,7 +146,7 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_quote_batch_order
c = Post.connection
- assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do
+ assert_sql(/ORDER BY #{Regexp.escape(c.quote_table_name("posts.id"))}/i) do
Post.find_in_batches(batch_size: 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 525085bb28..abce4565a4 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -139,6 +139,13 @@ class CalculationsTest < ActiveRecord::TestCase
end
end
+ def test_should_not_use_alias_for_grouped_field
+ assert_sql(/GROUP BY #{Regexp.escape(Account.connection.quote_table_name("accounts.firm_id"))}/i) do
+ c = Account.group(:firm_id).order("accounts_firm_id").sum(:credit_limit)
+ assert_equal [1, 2, 6, 9], c.keys.compact
+ end
+ end
+
def test_should_order_by_grouped_field
c = Account.group(:firm_id).order("firm_id").sum(:credit_limit)
assert_equal [1, 2, 6, 9], c.keys.compact
@@ -945,6 +952,90 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count)
end
+ def test_select_avg_with_group_by_as_virtual_attribute_with_sql
+ rails_core = companies(:rails_core)
+
+ sql = <<~SQL
+ SELECT firm_id, AVG(credit_limit) AS avg_credit_limit
+ FROM accounts
+ WHERE firm_id = ?
+ GROUP BY firm_id
+ LIMIT 1
+ SQL
+
+ account = Account.find_by_sql([sql, rails_core]).first
+
+ # id was not selected, so it should be nil
+ # (cannot select id because it wasn't used in the GROUP BY clause)
+ assert_nil account.id
+
+ # firm_id was explicitly selected, so it should be present
+ assert_equal(rails_core, account.firm)
+
+ # avg_credit_limit should be present as a virtual attribute
+ assert_equal(52.5, account.avg_credit_limit)
+ end
+
+ def test_select_avg_with_group_by_as_virtual_attribute_with_ar
+ rails_core = companies(:rails_core)
+
+ account = Account
+ .select(:firm_id, "AVG(credit_limit) AS avg_credit_limit")
+ .where(firm: rails_core)
+ .group(:firm_id)
+ .take!
+
+ # id was not selected, so it should be nil
+ # (cannot select id because it wasn't used in the GROUP BY clause)
+ assert_nil account.id
+
+ # firm_id was explicitly selected, so it should be present
+ assert_equal(rails_core, account.firm)
+
+ # avg_credit_limit should be present as a virtual attribute
+ assert_equal(52.5, account.avg_credit_limit)
+ end
+
+ def test_select_avg_with_joins_and_group_by_as_virtual_attribute_with_sql
+ rails_core = companies(:rails_core)
+
+ sql = <<~SQL
+ SELECT companies.*, AVG(accounts.credit_limit) AS avg_credit_limit
+ FROM companies
+ INNER JOIN accounts ON companies.id = accounts.firm_id
+ WHERE companies.id = ?
+ GROUP BY companies.id
+ LIMIT 1
+ SQL
+
+ firm = DependentFirm.find_by_sql([sql, rails_core]).first
+
+ # all the DependentFirm attributes should be present
+ assert_equal rails_core, firm
+ assert_equal rails_core.name, firm.name
+
+ # avg_credit_limit should be present as a virtual attribute
+ assert_equal(52.5, firm.avg_credit_limit)
+ end
+
+ def test_select_avg_with_joins_and_group_by_as_virtual_attribute_with_ar
+ rails_core = companies(:rails_core)
+
+ firm = DependentFirm
+ .select("companies.*", "AVG(accounts.credit_limit) AS avg_credit_limit")
+ .where(id: rails_core)
+ .joins(:account)
+ .group(:id)
+ .take!
+
+ # all the DependentFirm attributes should be present
+ assert_equal rails_core, firm
+ assert_equal rails_core.name, firm.name
+
+ # avg_credit_limit should be present as a virtual attribute
+ assert_equal(52.5, firm.avg_credit_limit)
+ end
+
def test_count_with_block_and_column_name_raises_an_error
assert_raises(ArgumentError) do
Account.count(:firm_id) { true }
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 27589966af..843242a897 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
def test_establish_connection_uses_spec_name
old_config = ActiveRecord::Base.configurations
- config = { "readonly" => { "adapter" => "sqlite3" } }
+ config = { "readonly" => { "adapter" => "sqlite3", "pool" => "5" } }
ActiveRecord::Base.configurations = config
resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations)
spec = resolver.spec(:readonly)
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index 515bf5df06..2ac249b478 100644
--- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -28,6 +28,22 @@ module ActiveRecord
resolver.resolve(spec, spec)
end
+ def test_invalid_string_config
+ config = { "foo" => "bar" }
+
+ assert_raises ActiveRecord::DatabaseConfigurations::InvalidConfigurationError do
+ resolve_config(config)
+ end
+ end
+
+ def test_invalid_symbol_config
+ config = { "foo" => :bar }
+
+ assert_raises ActiveRecord::DatabaseConfigurations::InvalidConfigurationError do
+ resolve_config(config)
+ end
+ end
+
def test_resolver_with_database_uri_and_current_env_symbol_key
ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
@@ -244,6 +260,25 @@ module ActiveRecord
assert_equal expected, actual
end
+ def test_no_url_sub_key_with_database_url_doesnt_trample_other_envs
+ ENV["DATABASE_URL"] = "postgres://localhost/baz"
+
+ config = { "default_env" => { "database" => "foo" }, "other_env" => { "url" => "postgres://foohost/bardb" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "database" => "baz",
+ "adapter" => "postgresql",
+ "host" => "localhost"
+ },
+ "other_env" =>
+ { "adapter" => "postgresql",
+ "database" => "bardb",
+ "host" => "foohost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
def test_merge_no_conflicts_with_database_url
ENV["DATABASE_URL"] = "postgres://localhost/foo"
@@ -273,6 +308,105 @@ module ActiveRecord
}
assert_equal expected, actual
end
+
+ def test_merge_no_conflicts_with_database_url_and_adapter
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
+
+ config = { "default_env" => { "adapter" => "postgresql", "pool" => "5" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => "5"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_merge_no_conflicts_with_database_url_and_numeric_pool
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
+
+ config = { "default_env" => { "pool" => 5 } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => 5
+ }
+ }
+
+ assert_equal expected, actual
+ end
+
+ def test_tiered_configs_with_database_url
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
+
+ config = {
+ "default_env" => {
+ "primary" => { "pool" => 5 },
+ "animals" => { "pool" => 5 }
+ }
+ }
+
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ actual = configs.configs_for(env_name: "default_env", spec_name: "primary").config
+ expected = {
+ "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => 5
+ }
+
+ assert_equal expected, actual
+
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ actual = configs.configs_for(env_name: "default_env", spec_name: "animals").config
+ expected = { "pool" => 5 }
+
+ assert_equal expected, actual
+ end
+
+ def test_separate_database_env_vars
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
+ ENV["PRIMARY_DATABASE_URL"] = "postgres://localhost/primary"
+ ENV["ANIMALS_DATABASE_URL"] = "postgres://localhost/animals"
+
+ config = {
+ "default_env" => {
+ "primary" => { "pool" => 5 },
+ "animals" => { "pool" => 5 }
+ }
+ }
+
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ actual = configs.configs_for(env_name: "default_env", spec_name: "primary").config
+ assert_equal "primary", actual["database"]
+
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ actual = configs.configs_for(env_name: "default_env", spec_name: "animals").config
+ assert_equal "animals", actual["database"]
+ ensure
+ ENV.delete("PRIMARY_DATABASE_URL")
+ ENV.delete("ANIMALS_DATABASE_URL")
+ end
+
+ def test_does_not_change_other_environments
+ ENV["DATABASE_URL"] = "postgres://localhost/foo"
+ config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" }, "default_env" => {} }
+
+ actual = resolve_spec(:production, config)
+ assert_equal config["production"].merge("name" => "production"), actual
+
+ actual = resolve_spec(:default_env, config)
+ assert_equal({
+ "host" => "localhost",
+ "database" => "foo",
+ "adapter" => "postgresql",
+ "name" => "default_env"
+ }, actual)
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
index bc823fd072..774380d7e0 100644
--- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -40,7 +40,7 @@ if current_adapter?(:Mysql2Adapter)
end
def test_enum_type_with_value_matching_other_type
- assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
+ assert_lookup_type :string, "ENUM('unicode', '8bit', 'none', 'time')"
end
def test_binary_types
diff --git a/activerecord/test/cases/database_configurations_test.rb b/activerecord/test/cases/database_configurations_test.rb
index ed8151f01a..714b9d75c5 100644
--- a/activerecord/test/cases/database_configurations_test.rb
+++ b/activerecord/test/cases/database_configurations_test.rb
@@ -80,17 +80,20 @@ class LegacyDatabaseConfigurationsTest < ActiveRecord::TestCase
def test_each_is_deprecated
assert_deprecated do
- ActiveRecord::Base.configurations.each do |db_config|
- assert_equal "primary", db_config.spec_name
+ all_configs = ActiveRecord::Base.configurations.values
+ ActiveRecord::Base.configurations.each do |env_name, config|
+ assert_includes ["arunit", "arunit2", "arunit_without_prepared_statements"], env_name
+ assert_includes all_configs, config
end
end
end
def test_first_is_deprecated
+ first_config = ActiveRecord::Base.configurations.configurations.map(&:config).first
assert_deprecated do
- db_config = ActiveRecord::Base.configurations.first
- assert_equal "arunit", db_config.env_name
- assert_equal "primary", db_config.spec_name
+ env_name, config = ActiveRecord::Base.configurations.first
+ assert_equal "arunit", env_name
+ assert_equal first_config, config
end
end
diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb
index fd02d2acb4..340151e6db 100644
--- a/activerecord/test/cases/database_selector_test.rb
+++ b/activerecord/test/cases/database_selector_test.rb
@@ -123,6 +123,40 @@ module ActiveRecord
assert read
end
+ def test_preventing_writes_turns_off_for_primary_write
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+
+ read = false
+ write = false
+ resolver.read do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ assert ActiveRecord::Base.connection_handler.prevent_writes
+ read = true
+
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ assert_not ActiveRecord::Base.connection_handler.prevent_writes
+ write = true
+ end
+ end
+
+ assert write
+ assert read
+ end
+
def test_read_from_replica_with_no_delay
resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 0.seconds)
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index a2a501a794..38baf0509c 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -491,6 +491,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal 4, pirate.previous_changes.size
assert_equal [nil, "arrr"], pirate.previous_changes["catchphrase"]
+ assert_nil pirate.catchphrase_previously_was
assert_equal [nil, pirate.id], pirate.previous_changes["id"]
assert_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
@@ -507,6 +508,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal 4, pirate.previous_changes.size
assert_equal [nil, "arrr"], pirate.previous_changes["catchphrase"]
+ assert_nil pirate.catchphrase_previously_was
assert_equal [nil, pirate.id], pirate.previous_changes["id"]
assert_includes pirate.previous_changes, "updated_on"
assert_includes pirate.previous_changes, "created_on"
@@ -525,6 +527,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal 2, pirate.previous_changes.size
assert_equal ["arrr", "Me Maties!"], pirate.previous_changes["catchphrase"]
+ assert_equal "arrr", pirate.catchphrase_previously_was
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
assert_not pirate.previous_changes.key?("parrot_id")
@@ -539,6 +542,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal 2, pirate.previous_changes.size
assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes["catchphrase"]
+ assert_equal "Me Maties!", pirate.catchphrase_previously_was
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
assert_not pirate.previous_changes.key?("parrot_id")
@@ -551,6 +555,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal 2, pirate.previous_changes.size
assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes["catchphrase"]
+ assert_equal "Thar She Blows!", pirate.catchphrase_previously_was
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
assert_not pirate.previous_changes.key?("parrot_id")
@@ -563,6 +568,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal 2, pirate.previous_changes.size
assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes["catchphrase"]
+ assert_equal "Ahoy!", pirate.catchphrase_previously_was
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
assert_not pirate.previous_changes.key?("parrot_id")
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index ae0ce195b3..0ae156320a 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -3,6 +3,7 @@
require "cases/helper"
require "models/author"
require "models/book"
+require "active_support/log_subscriber/test_helper"
class EnumTest < ActiveRecord::TestCase
fixtures :books, :authors, :author_addresses
@@ -565,4 +566,27 @@ class EnumTest < ActiveRecord::TestCase
assert_raises(NoMethodError) { klass.proposed }
end
+
+ test "enums with a negative condition log a warning" do
+ old_logger = ActiveRecord::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+
+ ActiveRecord::Base.logger = logger
+
+ expected_message = "An enum element in Book uses the prefix 'not_'."\
+ " This will cause a conflict with auto generated negative scopes."
+
+ Class.new(ActiveRecord::Base) do
+ def self.name
+ "Book"
+ end
+ silence_warnings do
+ enum status: [:sent, :not_sent]
+ end
+ end
+
+ assert_match(expected_message, logger.logged(:warn).first)
+ ensure
+ ActiveRecord::Base.logger = old_logger
+ end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 3aa610f86b..1f2058cc0a 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -245,7 +245,8 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_does_not_select_columns_without_alias
- assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do
+ c = Topic.connection
+ assert_sql(/SELECT 1 AS one FROM #{Regexp.escape(c.quote_table_name("topics"))}/i) do
Topic.exists?
end
end
@@ -282,6 +283,11 @@ class FinderTest < ActiveRecord::TestCase
assert_not Post.select(:body).distinct.offset(4).exists?
end
+ def test_exists_with_distinct_and_offset_and_eagerload_and_order
+ assert Post.eager_load(:comments).distinct.offset(10).merge(Comment.order(post_id: :asc)).exists?
+ assert_not Post.eager_load(:comments).distinct.offset(11).merge(Comment.order(post_id: :asc)).exists?
+ end
+
# Ensure +exists?+ runs without an error by excluding distinct value.
# See https://github.com/rails/rails/pull/26981.
def test_exists_with_order_and_distinct
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index a7f01e898e..0861d938c5 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -67,7 +67,7 @@ class FixturesTest < ActiveRecord::TestCase
end
def call(_, _, _, _, values)
- @events << values[:sql] if values[:sql] =~ /INSERT/
+ @events << values[:sql] if /INSERT/.match?(values[:sql])
end
end
@@ -1279,6 +1279,33 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
end
end
+class IgnoreFixturesTest < ActiveRecord::TestCase
+ fixtures :other_books, :parrots
+
+ test "ignores books fixtures" do
+ assert_raise(StandardError) { other_books(:published) }
+ assert_raise(StandardError) { other_books(:published_paperback) }
+ assert_raise(StandardError) { other_books(:published_ebook) }
+
+ assert_equal 2, Book.count
+ assert_equal "Agile Web Development with Rails", other_books(:awdr).name
+ assert_equal "published", other_books(:awdr).status
+ assert_equal "paperback", other_books(:awdr).format
+ assert_equal "english", other_books(:awdr).language
+
+ assert_equal "Ruby for Rails", other_books(:rfr).name
+ assert_equal "ebook", other_books(:rfr).format
+ assert_equal "published", other_books(:rfr).status
+ end
+
+ test "ignores parrots fixtures" do
+ assert_raise(StandardError) { parrots(:DEFAULT) }
+ assert_raise(StandardError) { parrots(:DEAD_PARROT) }
+
+ assert_equal "DeadParrot", parrots(:polly).parrot_sti_class
+ end
+end
+
class FixturesWithDefaultScopeTest < ActiveRecord::TestCase
fixtures :bulbs
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 629167e9ed..01e4878c3f 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -471,9 +471,9 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_eager_load_belongs_to_primary_key_quoting
- con = Account.connection
+ c = Account.connection
bind_param = Arel::Nodes::BindParam.new(nil)
- assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = (?:#{Regexp.quote(bind_param.to_sql)}|1)/) do
+ assert_sql(/#{Regexp.escape(c.quote_table_name("companies.id"))} = (?:#{Regexp.escape(bind_param.to_sql)}|1)/i) do
Account.all.merge!(includes: :firm).find(1)
end
end
diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb
index d086d77081..42c623fafb 100644
--- a/activerecord/test/cases/insert_all_test.rb
+++ b/activerecord/test/cases/insert_all_test.rb
@@ -2,6 +2,7 @@
require "cases/helper"
require "models/book"
+require "models/speedometer"
class ReadonlyNameBook < Book
attr_readonly :name
@@ -225,6 +226,23 @@ class InsertAllTest < ActiveRecord::TestCase
assert_equal new_name, Book.find(1).name
end
+ def test_upsert_all_updates_existing_record_by_primary_key
+ skip unless supports_insert_on_duplicate_update?
+
+ Book.upsert_all [{ id: 1, name: "New edition" }], unique_by: :id
+
+ assert_equal "New edition", Book.find(1).name
+ end
+
+ def test_upsert_all_updates_existing_record_by_configured_primary_key
+ skip unless supports_insert_on_duplicate_update?
+
+ error = assert_raises ArgumentError do
+ Speedometer.upsert_all [{ speedometer_id: "s1", name: "New Speedometer" }]
+ end
+ assert_match "No unique index found for speedometer_id", error.message
+ end
+
def test_upsert_all_does_not_update_readonly_attributes
skip unless supports_insert_on_duplicate_update?
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 53a4963909..79bd6906d1 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -536,6 +536,23 @@ class QueryCacheTest < ActiveRecord::TestCase
ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
end
+ test "query cache is enabled in threads with shared connection" do
+ ActiveRecord::Base.connection_pool.lock_thread = true
+
+ assert_cache :off
+
+ thread_a = Thread.new do
+ middleware { |env|
+ assert_cache :clean
+ [200, {}, nil]
+ }.call({})
+ end
+
+ thread_a.join
+
+ ActiveRecord::Base.connection_pool.lock_thread = false
+ end
+
private
def with_temporary_connection_pool
old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name)
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index e0743de94b..e74fb1a098 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -363,6 +363,13 @@ module ActiveRecord
assert_match %r{/\*\+ BADHINT \*/}, post_with_hint.to_sql
end
+ def test_does_not_duplicate_optimizer_hints_on_merge
+ escaped_table = Post.connection.quote_table_name("posts")
+ expected = "SELECT /*+ OMGHINT */ #{escaped_table}.* FROM #{escaped_table}"
+ query = Post.optimizer_hints("OMGHINT").merge(Post.optimizer_hints("OMGHINT")).to_sql
+ assert_equal expected, query
+ end
+
class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
def type
:string
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 5df1e3ccf9..8b3ae02947 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -308,9 +308,9 @@ class RelationTest < ActiveRecord::TestCase
end
def test_reverse_order_with_function_other_predicates
- topics = Topic.order(Arel.sql("author_name, length(title), id")).reverse_order
+ topics = Topic.order("author_name, length(title), id").reverse_order
assert_equal topics(:second).title, topics.first.title
- topics = Topic.order(Arel.sql("length(author_name), id, length(title)")).reverse_order
+ topics = Topic.order("length(author_name), id, length(title)").reverse_order
assert_equal topics(:fifth).title, topics.first.title
end
@@ -1932,7 +1932,7 @@ class RelationTest < ActiveRecord::TestCase
assert_no_queries do
result = authors_count.map do |post|
- [post.num_posts, post.author.try(:name)]
+ [post.num_posts, post.author&.name]
end
expected = [[1, nil], [5, "David"], [3, "Mary"], [2, "Bob"]]
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 258132835f..4b039395e8 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -7,7 +7,10 @@ if current_adapter?(:Mysql2Adapter)
module ActiveRecord
class MysqlDBCreateTest < ActiveRecord::TestCase
def setup
- @connection = Class.new { def create_database(*); end }.new
+ @connection = Class.new do
+ def create_database(*); end
+ def error_number(_); end
+ end.new
@configuration = {
"adapter" => "mysql2",
"database" => "my-app-db"
@@ -90,7 +93,7 @@ if current_adapter?(:Mysql2Adapter)
with_stubbed_connection_establish_connection do
ActiveRecord::Base.connection.stub(
:create_database,
- proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists }
+ proc { raise ActiveRecord::DatabaseAlreadyExists }
) do
ActiveRecord::Tasks::DatabaseTasks.create @configuration
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index f9df650687..d74ba0580d 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -129,7 +129,7 @@ if current_adapter?(:PostgreSQLAdapter)
with_stubbed_connection_establish_connection do
ActiveRecord::Base.connection.stub(
:create_database,
- proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists }
+ proc { raise ActiveRecord::DatabaseAlreadyExists }
) do
ActiveRecord::Tasks::DatabaseTasks.create @configuration
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index 2645776ab7..4dd8a4a82b 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -51,7 +51,7 @@ class I18nValidationTest < ActiveRecord::TestCase
test "validates_uniqueness_of on generated message #{name}" do
Topic.validates_uniqueness_of :title, validation_options
@topic.title = unique_topic.title
- assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(value: "unique!")]) do
+ assert_called_with(ActiveModel::Error, :generate_message, [:title, :taken, @topic, generate_message_options.merge(value: "unique!")]) do
@topic.valid?
@topic.errors.messages
end
@@ -61,7 +61,7 @@ class I18nValidationTest < ActiveRecord::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_associated on generated message #{name}" do
Topic.validates_associated :replies, validation_options
- assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(value: replied_topic.replies)]) do
+ assert_called_with(ActiveModel::Error, :generate_message, [:replies, :invalid, replied_topic, generate_message_options.merge(value: replied_topic.replies)]) do
replied_topic.save
replied_topic.errors.messages
end
diff --git a/activerecord/test/fixtures/other_books.yml b/activerecord/test/fixtures/other_books.yml
new file mode 100644
index 0000000000..62806c03d7
--- /dev/null
+++ b/activerecord/test/fixtures/other_books.yml
@@ -0,0 +1,26 @@
+_fixture:
+ model_class: Book
+ ignore:
+ - PUBLISHED
+ - PUBLISHED_PAPERBACK
+ - PUBLISHED_EBOOK
+
+PUBLISHED: &PUBLISHED
+ status: :published
+
+PUBLISHED_PAPERBACK: &PUBLISHED_PAPERBACK
+ <<: *PUBLISHED
+ format: "paperback"
+ language: :english
+
+PUBLISHED_EBOOK: &PUBLISHED_EBOOK
+ <<: *PUBLISHED
+ format: "ebook"
+
+awdr:
+ <<: *PUBLISHED_PAPERBACK
+ name: "Agile Web Development with Rails"
+
+rfr:
+ <<: *PUBLISHED_EBOOK
+ name: "Ruby for Rails"
diff --git a/activerecord/test/fixtures/parrots.yml b/activerecord/test/fixtures/parrots.yml
index 8425ef98e0..4f0a090e03 100644
--- a/activerecord/test/fixtures/parrots.yml
+++ b/activerecord/test/fixtures/parrots.yml
@@ -1,3 +1,9 @@
+_fixture:
+ ignore: DEAD_PARROT
+
+DEAD_PARROT: &DEAD_PARROT
+ parrot_sti_class: DeadParrot
+
george:
name: "Curious George"
treasures: diamond, sapphire
@@ -17,7 +23,7 @@ polly:
name: $LABEL
killer: blackbeard
treasures: sapphire, ruby
- parrot_sti_class: DeadParrot
+ <<: *DEAD_PARROT
DEFAULTS: &DEFAULTS
treasures: sapphire, ruby
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index b52b643ad7..da7e4139b1 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -116,6 +116,7 @@ class Author < ActiveRecord::Base
has_many :tags_with_primary_key, through: :posts
has_many :books
+ has_many :published_books, class_name: "PublishedBook"
has_many :unpublished_books, -> { where(status: [:proposed, :written]) }, class_name: "Book"
has_many :subscriptions, through: :books
has_many :subscribers, -> { order("subscribers.nick") }, through: :subscriptions
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index afdda1a81e..43b82e6047 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -24,3 +24,9 @@ class Book < ActiveRecord::Base
"do publish work..."
end
end
+
+class PublishedBook < ActiveRecord::Base
+ self.table_name = "books"
+
+ validates_uniqueness_of :isbn
+end
diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb
index 8258408f35..fe7f5c9e03 100644
--- a/activerecord/test/models/drink_designer.rb
+++ b/activerecord/test/models/drink_designer.rb
@@ -10,5 +10,11 @@ class DrinkDesignerWithPolymorphicDependentNullifyChef < ActiveRecord::Base
has_one :chef, as: :employable, dependent: :nullify
end
+class DrinkDesignerWithPolymorphicTouchChef < ActiveRecord::Base
+ self.table_name = "drink_designers"
+
+ has_one :chef, as: :employable, touch: true
+end
+
class MocktailDesigner < DrinkDesigner
end
diff --git a/activerecord/test/models/mouse.rb b/activerecord/test/models/mouse.rb
new file mode 100644
index 0000000000..75a55c125d
--- /dev/null
+++ b/activerecord/test/models/mouse.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Mouse < ActiveRecord::Base
+ has_many :squeaks, autosave: true
+ validates :name, presence: true
+end
diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb
index 7973219a79..6bab7a1eb9 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -27,7 +27,8 @@ class ShipWithoutNestedAttributes < ActiveRecord::Base
has_many :prisoners, inverse_of: :ship, foreign_key: :ship_id
has_many :parts, class_name: "ShipPart", foreign_key: :ship_id
- validates :name, presence: true
+ validates :name, presence: true, if: -> { true }
+ validates :name, presence: true, if: -> { true }
end
class Prisoner < ActiveRecord::Base
diff --git a/activerecord/test/models/squeak.rb b/activerecord/test/models/squeak.rb
new file mode 100644
index 0000000000..e0a643c238
--- /dev/null
+++ b/activerecord/test/models/squeak.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class Squeak < ActiveRecord::Base
+ belongs_to :mouse
+ accepts_nested_attributes_for :mouse
+end
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index b143035213..911ac808c6 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -62,10 +62,6 @@ ActiveRecord::Schema.define do
t.binary :binary_column, limit: 1
end
- create_table :enum_tests, id: false, force: true do |t|
- t.column :enum_column, "ENUM('text','blob','tiny','medium','long','unsigned','bigint')"
- end
-
execute "DROP PROCEDURE IF EXISTS ten"
execute <<~SQL
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index eed18a7b89..cae2890c9e 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -115,7 +115,7 @@ ActiveRecord::Schema.define do
t.column :font_size, :integer, **default_zero
t.column :difficulty, :integer, **default_zero
t.column :cover, :string, default: "hard"
- t.string :isbn
+ t.string :isbn, **case_sensitive_options
t.datetime :published_on
t.index [:author_id, :name], unique: true
t.index :isbn, where: "published_on IS NOT NULL", unique: true
@@ -563,6 +563,10 @@ ActiveRecord::Schema.define do
t.string :type
end
+ create_table :mice, force: true do |t|
+ t.string :name
+ end
+
create_table :movies, force: true, id: false do |t|
t.primary_key :movieid
t.string :name
@@ -843,6 +847,10 @@ ActiveRecord::Schema.define do
end
end
+ create_table :squeaks, force: true do |t|
+ t.integer :mouse_id
+ end
+
create_table :prisoners, force: true do |t|
t.belongs_to :ship
end
@@ -1062,6 +1070,7 @@ ActiveRecord::Schema.define do
create_table :cake_designers, force: true do |t|
end
create_table :drink_designers, force: true do |t|
+ t.string :name
end
create_table :chefs, force: true do |t|
t.integer :employable_id
@@ -1069,6 +1078,7 @@ ActiveRecord::Schema.define do
t.integer :department_id
t.string :employable_list_type
t.integer :employable_list_id
+ t.timestamps
end
create_table :recipes, force: true do |t|
t.integer :chef_id
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
index fdb0f143f4..1475a7a786 100644
--- a/activestorage/CHANGELOG.md
+++ b/activestorage/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Add `config.active_storage.draw_routes` to disable Active Storage routes.
+
+ *Gannon McGibbon*
+
* Image analysis is skipped if ImageMagick returns an error.
`ActiveStorage::Analyzer::ImageAnalyzer#metadata` would previously raise a
diff --git a/activestorage/app/jobs/active_storage/mirror_job.rb b/activestorage/app/jobs/active_storage/mirror_job.rb
index e34faedb56..e70629d6ec 100644
--- a/activestorage/app/jobs/active_storage/mirror_job.rb
+++ b/activestorage/app/jobs/active_storage/mirror_job.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/object/try"
+
# Provides asynchronous mirroring of directly-uploaded blobs.
class ActiveStorage::MirrorJob < ActiveStorage::BaseJob
queue_as { ActiveStorage.queues[:mirror] }
diff --git a/activestorage/config/routes.rb b/activestorage/config/routes.rb
index 3af7361cff..bde53e72f3 100644
--- a/activestorage/config/routes.rb
+++ b/activestorage/config/routes.rb
@@ -29,4 +29,4 @@ Rails.application.routes.draw do
resolve("ActiveStorage::Blob") { |blob, options| route_for(:rails_blob, blob, options) }
resolve("ActiveStorage::Attachment") { |attachment, options| route_for(:rails_blob, attachment.blob, options) }
-end
+end if ActiveStorage.draw_routes
diff --git a/activestorage/lib/active_storage.rb b/activestorage/lib/active_storage.rb
index 5c5da551ae..c35a9920d6 100644
--- a/activestorage/lib/active_storage.rb
+++ b/activestorage/lib/active_storage.rb
@@ -43,17 +43,26 @@ module ActiveStorage
mattr_accessor :logger
mattr_accessor :verifier
+ mattr_accessor :variant_processor, default: :mini_magick
+
mattr_accessor :queues, default: {}
+
mattr_accessor :previewers, default: []
- mattr_accessor :analyzers, default: []
- mattr_accessor :variant_processor, default: :mini_magick
+ mattr_accessor :analyzers, default: []
+
mattr_accessor :paths, default: {}
- mattr_accessor :variable_content_types, default: []
+
+ mattr_accessor :variable_content_types, default: []
+ mattr_accessor :binary_content_type, default: "application/octet-stream"
mattr_accessor :content_types_to_serve_as_binary, default: []
- mattr_accessor :content_types_allowed_inline, default: []
- mattr_accessor :binary_content_type, default: "application/octet-stream"
+ mattr_accessor :content_types_allowed_inline, default: []
+
mattr_accessor :service_urls_expire_in, default: 5.minutes
+
mattr_accessor :routes_prefix, default: "/rails/active_storage"
+ mattr_accessor :draw_routes, default: true
+
+ mattr_accessor :replace_on_assign_to_many, default: false
module Transformers
extend ActiveSupport::Autoload
diff --git a/activestorage/lib/active_storage/attached/model.rb b/activestorage/lib/active_storage/attached/model.rb
index ae7f0685f2..4bd6b56b6c 100644
--- a/activestorage/lib/active_storage/attached/model.rb
+++ b/activestorage/lib/active_storage/attached/model.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/object/try"
+
module ActiveStorage
# Provides the class-level DSL for declaring an Active Record model's attachments.
module Attached::Model
@@ -93,12 +95,19 @@ module ActiveStorage
end
def #{name}=(attachables)
- attachment_changes["#{name}"] =
- if attachables.nil? || Array(attachables).none?
- ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
- else
- ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
+ if ActiveStorage.replace_on_assign_to_many
+ attachment_changes["#{name}"] =
+ if attachables.nil? || Array(attachables).none?
+ ActiveStorage::Attached::Changes::DeleteMany.new("#{name}", self)
+ else
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, attachables)
+ end
+ else
+ if !attachables.nil? || Array(attachables).any?
+ attachment_changes["#{name}"] =
+ ActiveStorage::Attached::Changes::CreateMany.new("#{name}", self, #{name}.blobs + attachables)
end
+ end
end
CODE
diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb
index f70d0a512a..9d9cd02d12 100644
--- a/activestorage/lib/active_storage/engine.rb
+++ b/activestorage/lib/active_storage/engine.rb
@@ -73,12 +73,15 @@ module ActiveStorage
ActiveStorage.analyzers = app.config.active_storage.analyzers || []
ActiveStorage.paths = app.config.active_storage.paths || {}
ActiveStorage.routes_prefix = app.config.active_storage.routes_prefix || "/rails/active_storage"
+ ActiveStorage.draw_routes = app.config.active_storage.draw_routes != false
ActiveStorage.variable_content_types = app.config.active_storage.variable_content_types || []
ActiveStorage.content_types_to_serve_as_binary = app.config.active_storage.content_types_to_serve_as_binary || []
ActiveStorage.service_urls_expire_in = app.config.active_storage.service_urls_expire_in || 5.minutes
ActiveStorage.content_types_allowed_inline = app.config.active_storage.content_types_allowed_inline || []
ActiveStorage.binary_content_type = app.config.active_storage.binary_content_type || "application/octet-stream"
+
+ ActiveStorage.replace_on_assign_to_many = app.config.active_storage.replace_on_assign_to_many || false
end
end
diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb
index 993cc0e5f7..0648da70b5 100644
--- a/activestorage/lib/active_storage/service/azure_storage_service.rb
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -17,10 +17,12 @@ module ActiveStorage
@container = container
end
- def upload(key, io, checksum: nil, **)
+ def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
instrument :upload, key: key, checksum: checksum do
handle_errors do
- blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum)
+ content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
+
+ blobs.create_block_blob(container, key, IO.try_convert(io) || io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition)
end
end
end
diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb
index 67892d43b2..764a447c69 100644
--- a/activestorage/lib/active_storage/service/disk_service.rb
+++ b/activestorage/lib/active_storage/service/disk_service.rb
@@ -84,8 +84,12 @@ module ActiveStorage
purpose: :blob_key }
)
+ current_uri = URI.parse(current_host)
+
generated_url = url_helpers.rails_disk_service_url(verified_key_with_expiration,
- host: current_host,
+ protocol: current_uri.scheme,
+ host: current_uri.host,
+ port: current_uri.port,
disposition: content_disposition,
content_type: content_type,
filename: filename
diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb
index e4bd57048a..a73f6ab526 100644
--- a/activestorage/lib/active_storage/service/s3_service.rb
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -20,12 +20,14 @@ module ActiveStorage
@upload_options = upload
end
- def upload(key, io, checksum: nil, content_type: nil, **)
+ def upload(key, io, checksum: nil, filename: nil, content_type: nil, disposition: nil, **)
instrument :upload, key: key, checksum: checksum do
+ content_disposition = content_disposition_with(filename: filename, type: disposition) if disposition && filename
+
if io.size < multipart_upload_threshold
- upload_with_single_part key, io, checksum: checksum, content_type: content_type
+ upload_with_single_part key, io, checksum: checksum, content_type: content_type, content_disposition: content_disposition
else
- upload_with_multipart key, io, content_type: content_type
+ upload_with_multipart key, io, content_type: content_type, content_disposition: content_disposition
end
end
end
@@ -103,16 +105,16 @@ module ActiveStorage
MAXIMUM_UPLOAD_PARTS_COUNT = 10000
MINIMUM_UPLOAD_PART_SIZE = 5.megabytes
- def upload_with_single_part(key, io, checksum: nil, content_type: nil)
- object_for(key).put(body: io, content_md5: checksum, content_type: content_type, **upload_options)
+ def upload_with_single_part(key, io, checksum: nil, content_type: nil, content_disposition: nil)
+ object_for(key).put(body: io, content_md5: checksum, content_type: content_type, content_disposition: content_disposition, **upload_options)
rescue Aws::S3::Errors::BadDigest
raise ActiveStorage::IntegrityError
end
- def upload_with_multipart(key, io, content_type: nil)
+ def upload_with_multipart(key, io, content_type: nil, content_disposition: nil)
part_size = [ io.size.fdiv(MAXIMUM_UPLOAD_PARTS_COUNT).ceil, MINIMUM_UPLOAD_PART_SIZE ].max
- object_for(key).upload_stream(content_type: content_type, part_size: part_size, **upload_options) do |out|
+ object_for(key).upload_stream(content_type: content_type, content_disposition: content_disposition, part_size: part_size, **upload_options) do |out|
IO.copy_stream(io, out)
end
end
diff --git a/activestorage/test/analyzer/video_analyzer_test.rb b/activestorage/test/analyzer/video_analyzer_test.rb
index d30f49315a..172a2f0aae 100644
--- a/activestorage/test/analyzer/video_analyzer_test.rb
+++ b/activestorage/test/analyzer/video_analyzer_test.rb
@@ -24,7 +24,6 @@ class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
assert_equal 480, metadata[:width]
assert_equal 640, metadata[:height]
assert_equal [4, 3], metadata[:display_aspect_ratio]
- assert_equal 5.227975, metadata[:duration]
assert_equal 90, metadata[:angle]
end
diff --git a/activestorage/test/dummy/config/initializers/backtrace_silencers.rb b/activestorage/test/dummy/config/initializers/backtrace_silencers.rb
index d0f0d3b5df..8452ef9236 100644
--- a/activestorage/test/dummy/config/initializers/backtrace_silencers.rb
+++ b/activestorage/test/dummy/config/initializers/backtrace_silencers.rb
@@ -2,7 +2,7 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!
diff --git a/activestorage/test/fixtures/files/racecar.tif b/activestorage/test/fixtures/files/racecar.tif
index 0a11b22896..97430741e2 100644
--- a/activestorage/test/fixtures/files/racecar.tif
+++ b/activestorage/test/fixtures/files/racecar.tif
Binary files differ
diff --git a/activestorage/test/models/attached/many_test.rb b/activestorage/test/models/attached/many_test.rb
index 878e284049..39ddecb041 100644
--- a/activestorage/test/models/attached/many_test.rb
+++ b/activestorage/test/models/attached/many_test.rb
@@ -269,6 +269,24 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
end
end
+ test "updating an existing record with attachments when appending on assign" do
+ append_on_assign do
+ @user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
+
+ assert_difference -> { @user.reload.highlights.count }, +2 do
+ @user.update! highlights: [ create_blob(filename: "whenever.jpg"), create_blob(filename: "wherever.jpg") ]
+ end
+
+ assert_no_difference -> { @user.reload.highlights.count } do
+ @user.update! highlights: [ ]
+ end
+
+ assert_no_difference -> { @user.reload.highlights.count } do
+ @user.update! highlights: nil
+ end
+ end
+ end
+
test "attaching existing blobs to a new record" do
User.new(name: "Jason").tap do |user|
user.highlights.attach create_blob(filename: "funky.jpg"), create_blob(filename: "town.jpg")
@@ -538,4 +556,12 @@ class ActiveStorage::ManyAttachedTest < ActiveSupport::TestCase
User.remove_method :highlights
end
end
+
+ private
+ def append_on_assign
+ ActiveStorage.replace_on_assign_to_many, previous = false, ActiveStorage.replace_on_assign_to_many
+ yield
+ ensure
+ ActiveStorage.replace_on_assign_to_many = previous
+ end
end
diff --git a/activestorage/test/service/azure_storage_service_test.rb b/activestorage/test/service/azure_storage_service_test.rb
index 2b07902d07..9eea1b94c8 100644
--- a/activestorage/test/service/azure_storage_service_test.rb
+++ b/activestorage/test/service/azure_storage_service_test.rb
@@ -9,6 +9,35 @@ if SERVICE_CONFIGURATIONS[:azure]
include ActiveStorage::Service::SharedServiceTests
+ test "upload with content_type" do
+ key = SecureRandom.base58(24)
+ data = "Foobar"
+
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data), filename: ActiveStorage::Filename.new("test.txt"), content_type: "text/plain")
+
+ url = @service.url(key, expires_in: 2.minutes, disposition: :attachment, content_type: nil, filename: ActiveStorage::Filename.new("test.html"))
+ response = Net::HTTP.get_response(URI(url))
+ assert_equal "text/plain", response.content_type
+ assert_match(/attachment;.*test\.html/, response["Content-Disposition"])
+ ensure
+ @service.delete key
+ end
+
+ test "upload with content disposition" do
+ key = SecureRandom.base58(24)
+ data = "Foobar"
+
+ @service.upload(key, StringIO.new(data), checksum: Digest::MD5.base64digest(data), filename: ActiveStorage::Filename.new("test.txt"), disposition: :inline)
+
+ assert_equal("inline; filename=\"test.txt\"; filename*=UTF-8''test.txt", @service.blobs.get_blob_properties(@service.container, key).properties[:content_disposition])
+
+ url = @service.url(key, expires_in: 2.minutes, disposition: :attachment, content_type: nil, filename: ActiveStorage::Filename.new("test.html"))
+ response = Net::HTTP.get_response(URI(url))
+ assert_match(/attachment;.*test\.html/, response["Content-Disposition"])
+ ensure
+ @service.delete key
+ end
+
test "signed URL generation" do
url = @service.url(@key, expires_in: 5.minutes,
disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")
diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb
index f3c4dd26bd..b766cc3f56 100644
--- a/activestorage/test/service/disk_service_test.rb
+++ b/activestorage/test/service/disk_service_test.rb
@@ -8,8 +8,14 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase
include ActiveStorage::Service::SharedServiceTests
test "URL generation" do
- assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
- @service.url(@key, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
+ original_url_options = Rails.application.routes.default_url_options.dup
+ Rails.application.routes.default_url_options.merge!(protocol: "http", host: "test.example.com", port: 3001)
+ begin
+ assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/,
+ @service.url(@key, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png"))
+ ensure
+ Rails.application.routes.default_url_options = original_url_options
+ end
end
test "headers_for_direct_upload generation" do
diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb
index b9120770e6..3dd1b59681 100644
--- a/activestorage/test/service/s3_service_test.rb
+++ b/activestorage/test/service/s3_service_test.rb
@@ -77,6 +77,23 @@ if SERVICE_CONFIGURATIONS[:s3]
@service.delete key
end
+ test "upload with content disposition" do
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+
+ @service.upload(
+ key,
+ StringIO.new(data),
+ checksum: Digest::MD5.base64digest(data),
+ filename: ActiveStorage::Filename.new("cool_data.txt"),
+ disposition: :attachment
+ )
+
+ assert_equal("attachment; filename=\"cool_data.txt\"; filename*=UTF-8''cool_data.txt", @service.bucket.object(key).content_disposition)
+ ensure
+ @service.delete key
+ end
+
test "uploading a large object in multiple parts" do
service = build_service(upload: { multipart_threshold: 5.megabytes })
diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb
index ac38b9362c..164b0acd96 100644
--- a/activestorage/test/test_helper.rb
+++ b/activestorage/test/test_helper.rb
@@ -6,6 +6,7 @@ require_relative "dummy/config/environment.rb"
require "bundler/setup"
require "active_support"
require "active_support/test_case"
+require "active_support/core_ext/object/try"
require "active_support/testing/autorun"
require "active_storage/service/mirror_service"
require "image_processing/mini_magick"
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 29b22bb3f9..dad7005eb1 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,17 +1,62 @@
-* Allow the on_rotation proc used when decrypting/verifying a message to be
- be passed at the constructor level.
+* Allow initializing `thread_mattr_*` attributes via `:default` option
+
+ class Scraper
+ thread_mattr_reader :client, default: Api::Client.new
+ end
+
+ *Guilherme Mansur*
+
+* Add `compact_blank` for those times when you want to remove #blank? values from
+ an Enumerable (also `compact_blank!` on Hash, Array, ActionController::Parameters)
+
+ *Dana Sherson*
+
+* Make ActiveSupport::Logger Fiber-safe. Fixes #36752.
+
+ Use `Fiber.current.__id__` in `ActiveSupport::Logger#local_level=` in order
+ to make log level local to Ruby Fibers in addition to Threads.
+
+ Example:
+
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.level = 1
+ p "Main is debug? #{logger.debug?}"
+
+ Fiber.new {
+ logger.local_level = 0
+ p "Thread is debug? #{logger.debug?}"
+ }.resume
+
+ p "Main is debug? #{logger.debug?}"
+
+ Before:
+
+ Main is debug? false
+ Thread is debug? true
+ Main is debug? true
+
+ After:
+
+ Main is debug? false
+ Thread is debug? true
+ Main is debug? false
+
+ *Alexander Varnin*
+
+* Allow the `on_rotation` proc used when decrypting/verifying a message to be
+ passed at the constructor level.
Before:
- crypt = ActiveSupport::MessageEncryptor.new('long_secret')
- crypt.decrypt_and_verify(encrypted_message, on_rotation: proc { ... })
- crypt.decrypt_and_verify(another_encrypted_message, on_rotation: proc { ... })
+ crypt = ActiveSupport::MessageEncryptor.new('long_secret')
+ crypt.decrypt_and_verify(encrypted_message, on_rotation: proc { ... })
+ crypt.decrypt_and_verify(another_encrypted_message, on_rotation: proc { ... })
After:
- crypt = ActiveSupport::MessageEncryptor.new('long_secret', on_rotation: proc { ... })
- crypt.decrypt_and_verify(encrypted_message)
- crypt.decrypt_and_verify(another_encrypted_message)
+ crypt = ActiveSupport::MessageEncryptor.new('long_secret', on_rotation: proc { ... })
+ crypt.decrypt_and_verify(encrypted_message)
+ crypt.decrypt_and_verify(another_encrypted_message)
*Edouard Chin*
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index 7ecbafc911..da295a0d35 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -34,5 +34,5 @@ Gem::Specification.new do |s|
s.add_dependency "tzinfo", "~> 1.1"
s.add_dependency "minitest", "~> 5.1"
s.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2"
- s.add_dependency "zeitwerk", "~> 2.1", ">= 2.1.4"
+ s.add_dependency "zeitwerk", "~> 2.1", ">= 2.1.8"
end
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index f55e821e10..6273012808 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -16,7 +16,7 @@ module ActiveSupport
#
# bc = ActiveSupport::BacktraceCleaner.new
# bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
- # bc.add_silencer { |line| line =~ /puma|rubygems/ } # skip any lines from puma or rubygems
+ # bc.add_silencer { |line| /puma|rubygems/.match?(line) } # skip any lines from puma or rubygems
# bc.clean(exception.backtrace) # perform the cleanup
#
# To reconfigure an existing BacktraceCleaner (like the default one in Rails)
@@ -65,7 +65,7 @@ module ActiveSupport
# for a given line, it will be excluded from the clean backtrace.
#
# # Will reject all lines that include the word "puma", like "/gems/puma/server.rb" or "/app/my_puma_server/rb"
- # backtrace_cleaner.add_silencer { |line| line =~ /puma/ }
+ # backtrace_cleaner.add_silencer { |line| /puma/.match?(line) }
def add_silencer(&block)
@silencers << block
end
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index e055135bb4..a42553ae3a 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -7,6 +7,7 @@ require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/numeric/bytes"
require "active_support/core_ext/numeric/time"
require "active_support/core_ext/object/to_param"
+require "active_support/core_ext/object/try"
require "active_support/core_ext/string/inflections"
module ActiveSupport
@@ -678,18 +679,15 @@ module ActiveSupport
end
def instrument(operation, key, options = nil)
- log { "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" }
+ if logger && logger.debug? && !silence?
+ logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
+ end
payload = { key: key }
payload.merge!(options) if options.is_a?(Hash)
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
end
- def log
- return unless logger && logger.debug? && !silence?
- logger.debug(yield)
- end
-
def handle_expired_entry(entry, key, options)
if entry && entry.expired?
race_ttl = options[:race_condition_ttl].to_i
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index 39b32fc7f6..8e80946fbb 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/object/duplicable"
require "active_support/core_ext/string/inflections"
require "active_support/per_thread_registry"
@@ -75,7 +74,10 @@ module ActiveSupport
end
def fetch_entry(key, options = nil) # :nodoc:
- @data.fetch(key) { @data[key] = yield }
+ entry = @data.fetch(key) { @data[key] = yield }
+ dup_entry = entry.dup
+ dup_entry&.dup_value!
+ dup_entry
end
end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index 11746e0537..daf98c9528 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -7,7 +7,6 @@ require "active_support/core_ext/class/attribute"
require "active_support/core_ext/kernel/reporting"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/string/filters"
-require "active_support/deprecation"
require "thread"
module ActiveSupport
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index c7a2378e41..27cb47eb6e 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/core_ext/object/try"
+require "active_support/core_ext/date_time/conversions"
module DateAndTime
module Calculations
diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb
index 4675c41936..728d332306 100644
--- a/activesupport/lib/active_support/core_ext/enumerable.rb
+++ b/activesupport/lib/active_support/core_ext/enumerable.rb
@@ -148,6 +148,41 @@ module Enumerable
map { |element| element[keys.first] }
end
end
+
+ # Returns a new +Array+ without the blank items.
+ # Uses Object#blank? for determining if an item is blank.
+ #
+ # [1, "", nil, 2, " ", [], {}, false, true].compact_blank
+ # # => [1, 2, true]
+ #
+ # Set.new([nil, "", 1, 2])
+ # # => [2, 1] (or [1, 2])
+ #
+ # When called on a +Hash+, returns a new +Hash+ without the blank values.
+ #
+ # { a: "", b: 1, c: nil, d: [], e: false, f: true }.compact_blank
+ # #=> { b: 1, f: true }
+ def compact_blank
+ reject(&:blank?)
+ end
+end
+
+class Hash
+ # Hash#reject has its own definition, so this needs one too.
+ def compact_blank #:nodoc:
+ reject { |_k, v| v.blank? }
+ end
+
+ # Removes all blank values from the +Hash+ in place and returns self.
+ # Uses Object#blank? for determining if a value is blank.
+ #
+ # h = { a: "", b: 1, c: nil, d: [], e: false, f: true }
+ # h.compact_blank!
+ # # => { b: 1, f: true }
+ def compact_blank!
+ # use delete_if rather than reject! because it always returns self even if nothing changed
+ delete_if { |_k, v| v.blank? }
+ end
end
class Range #:nodoc:
@@ -185,4 +220,15 @@ class Array #:nodoc:
super
end
end
+
+ # Removes all blank elements from the +Array+ in place and returns self.
+ # Uses Object#blank? for determining if an item is blank.
+ #
+ # a = [1, "", nil, 2, " ", [], {}, false, true]
+ # a.compact_blank!
+ # # => [1, 2, true]
+ def compact_blank!
+ # use delete_if rather than reject! because it always returns self even if nothing changed
+ delete_if(&:blank?)
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index 5b48254646..cc42647879 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
require "active_support/xml_mini"
-require "active_support/time"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/object/to_param"
require "active_support/core_ext/object/to_query"
+require "active_support/core_ext/object/try"
require "active_support/core_ext/array/wrap"
require "active_support/core_ext/hash/reverse_merge"
require "active_support/core_ext/string/inflections"
diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb
index 0c72cd7b47..5ff0e34d82 100644
--- a/activesupport/lib/active_support/core_ext/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/core_ext/string/inflections"
+
module ActiveSupport
module MarshalWithAutoloading # :nodoc:
def load(source, proc = nil)
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
index a6e87aeb68..ea4034303e 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
@@ -33,7 +33,7 @@ class Module
# end
#
# Current.new.user # => NoMethodError
- def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true) # :nodoc:
+ def thread_mattr_reader(*syms, instance_reader: true, instance_accessor: true, default: nil) # :nodoc:
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
@@ -52,6 +52,8 @@ class Module
end
EOS
end
+
+ Thread.current["attr_" + name + "_#{sym}"] = default unless default.nil?
end
end
alias :thread_cattr_reader :thread_mattr_reader
@@ -74,7 +76,7 @@ class Module
# end
#
# Current.new.user = "DHH" # => NoMethodError
- def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true) # :nodoc:
+ def thread_mattr_writer(*syms, instance_writer: true, instance_accessor: true, default: nil) # :nodoc:
syms.each do |sym|
raise NameError.new("invalid attribute name: #{sym}") unless /^[_A-Za-z]\w*$/.match?(sym)
@@ -93,6 +95,8 @@ class Module
end
EOS
end
+
+ public_send("#{sym}=", default) unless default.nil?
end
end
alias :thread_cattr_writer :thread_mattr_writer
@@ -136,8 +140,8 @@ class Module
#
# Current.new.user = "DHH" # => NoMethodError
# Current.new.user # => NoMethodError
- def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true)
- thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor)
+ def thread_mattr_accessor(*syms, instance_reader: true, instance_writer: true, instance_accessor: true, default: nil)
+ thread_mattr_reader(*syms, instance_reader: instance_reader, instance_accessor: instance_accessor, default: default)
thread_mattr_writer(*syms, instance_writer: instance_writer, instance_accessor: instance_accessor)
end
alias :thread_cattr_accessor :thread_mattr_accessor
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index 54271a3970..14d7f0c484 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -276,6 +276,11 @@ class Module
# The delegated method must be public on the target, otherwise it will
# raise +DelegationError+. If you wish to instead return +nil+,
# use the <tt>:allow_nil</tt> option.
+ #
+ # The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
+ # delegation due to possible interference when calling
+ # <tt>Marshal.dump(object)</tt>, should the delegation target method
+ # of <tt>object</tt> add or remove instance variables.
def delegate_missing_to(target, allow_nil: nil)
target = target.to_s
target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)
@@ -285,6 +290,7 @@ class Module
# It may look like an oversight, but we deliberately do not pass
# +include_private+, because they do not get delegated.
+ return false if name == :marshal_dump || name == :_dump
#{target}.respond_to?(name) || super
end
diff --git a/activesupport/lib/active_support/core_ext/name_error.rb b/activesupport/lib/active_support/core_ext/name_error.rb
index 6d37cd9dfd..c51aec0e03 100644
--- a/activesupport/lib/active_support/core_ext/name_error.rb
+++ b/activesupport/lib/active_support/core_ext/name_error.rb
@@ -15,7 +15,7 @@ class NameError
# We should use original_message message instead.
message = respond_to?(:original_message) ? original_message : self.message
- if /undefined local variable or method/ !~ message
+ unless /undefined local variable or method/.match?(message)
$1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index c78ee6bbfc..3ebcdca02b 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -28,96 +28,6 @@ class Object
end
end
-class NilClass
- begin
- nil.dup
- rescue TypeError
-
- # +nil+ is not duplicable:
- #
- # nil.duplicable? # => false
- # nil.dup # => TypeError: can't dup NilClass
- def duplicable?
- false
- end
- end
-end
-
-class FalseClass
- begin
- false.dup
- rescue TypeError
-
- # +false+ is not duplicable:
- #
- # false.duplicable? # => false
- # false.dup # => TypeError: can't dup FalseClass
- def duplicable?
- false
- end
- end
-end
-
-class TrueClass
- begin
- true.dup
- rescue TypeError
-
- # +true+ is not duplicable:
- #
- # true.duplicable? # => false
- # true.dup # => TypeError: can't dup TrueClass
- def duplicable?
- false
- end
- end
-end
-
-class Symbol
- begin
- :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:
- #
- # :my_symbol.duplicable? # => false
- # :my_symbol.dup # => TypeError: can't dup Symbol
- def duplicable?
- false
- end
- end
-end
-
-class Numeric
- begin
- 1.dup
- rescue TypeError
-
- # Numbers are not duplicable:
- #
- # 3.duplicable? # => false
- # 3.dup # => TypeError: can't dup Integer
- def duplicable?
- false
- end
- end
-end
-
-require "bigdecimal"
-class BigDecimal
- # BigDecimals are duplicable:
- #
- # BigDecimal("1.2").duplicable? # => true
- # BigDecimal("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)>
- def duplicable?
- true
- end
-end
-
class Method
# Methods are not duplicable:
#
@@ -128,32 +38,12 @@ class Method
end
end
-class Complex
- begin
- Complex(1).dup
- rescue TypeError
-
- # Complexes are not duplicable:
- #
- # Complex(1).duplicable? # => false
- # Complex(1).dup # => TypeError: can't copy Complex
- def duplicable?
- false
- end
- end
-end
-
-class Rational
- begin
- Rational(1).dup
- rescue TypeError
-
- # Rationals are not duplicable:
- #
- # Rational(1).duplicable? # => false
- # Rational(1).dup # => TypeError: can't copy Rational
- def duplicable?
- false
- end
+class UnboundMethod
+ # Unbound methods are not duplicable:
+ #
+ # method(:puts).unbind.duplicable? # => false
+ # method(:puts).unbind.dup # => TypeError: allocator undefined for UnboundMethod
+ def duplicable?
+ false
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb
index 4ca24028b0..f6a14c08bc 100644
--- a/activesupport/lib/active_support/core_ext/string/access.rb
+++ b/activesupport/lib/active_support/core_ext/string/access.rb
@@ -44,7 +44,7 @@ class String
# str.from(0).to(-1) # => "hello"
# str.from(1).to(-2) # => "ell"
def from(position)
- self[position..-1]
+ self[position, length]
end
# Returns a substring from the beginning of the string to the given position.
@@ -61,7 +61,8 @@ class String
# str.from(0).to(-1) # => "hello"
# str.from(1).to(-2) # => "ell"
def to(position)
- self[0..position]
+ position += size if position < 0
+ self[0, position + 1] || +""
end
# Returns the first character. If a limit is supplied, returns a substring
@@ -75,17 +76,7 @@ class String
# str.first(0) # => ""
# str.first(6) # => "hello"
def first(limit = 1)
- ActiveSupport::Deprecation.warn(
- "Calling String#first with a negative integer limit " \
- "will raise an ArgumentError in Rails 6.1."
- ) if limit < 0
- if limit == 0
- ""
- elsif limit >= size
- dup
- else
- to(limit - 1)
- end
+ self[0, limit] || raise(ArgumentError, "negative limit")
end
# Returns the last character of the string. If a limit is supplied, returns a substring
@@ -99,16 +90,6 @@ class String
# str.last(0) # => ""
# str.last(6) # => "hello"
def last(limit = 1)
- ActiveSupport::Deprecation.warn(
- "Calling String#last with a negative integer limit " \
- "will raise an ArgumentError in Rails 6.1."
- ) if limit < 0
- if limit == 0
- ""
- elsif limit >= size
- dup
- else
- from(-limit)
- end
+ self[[length - limit, 0].max, limit] || raise(ArgumentError, "negative limit")
end
end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 5dc47b20c6..6d71a67a46 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -20,6 +20,9 @@ module ActiveSupport #:nodoc:
module Dependencies #:nodoc:
extend self
+ UNBOUND_METHOD_MODULE_NAME = Module.instance_method(:name)
+ private_constant :UNBOUND_METHOD_MODULE_NAME
+
mattr_accessor :interlock, default: Interlock.new
# :doc:
@@ -201,6 +204,11 @@ module ActiveSupport #:nodoc:
end
end
+ def self.include_into(base)
+ base.include(self)
+ append_features(base)
+ end
+
def const_missing(const_name)
from_mod = anonymous? ? guess_for_anonymous(const_name) : self
Dependencies.load_missing_constant(from_mod, const_name)
@@ -230,6 +238,21 @@ module ActiveSupport #:nodoc:
base.class_eval do
define_method(:load, Kernel.instance_method(:load))
private :load
+
+ define_method(:require, Kernel.instance_method(:require))
+ private :require
+ end
+ end
+
+ def self.include_into(base)
+ base.include(self)
+
+ if base.instance_method(:load).owner == base
+ base.remove_method(:load)
+ end
+
+ if base.instance_method(:require).owner == base
+ base.remove_method(:require)
end
end
@@ -325,9 +348,9 @@ module ActiveSupport #:nodoc:
end
def hook!
- Object.class_eval { include Loadable }
- Module.class_eval { include ModuleConstMissing }
- Exception.class_eval { include Blamable }
+ Loadable.include_into(Object)
+ ModuleConstMissing.include_into(Module)
+ Exception.include(Blamable)
end
def unhook!
@@ -638,7 +661,7 @@ module ActiveSupport #:nodoc:
# Determine if the given constant has been automatically loaded.
def autoloaded?(desc)
- return false if desc.is_a?(Module) && desc.anonymous?
+ return false if desc.is_a?(Module) && real_mod_name(desc).nil?
name = to_constant_name desc
return false unless qualified_const_defined?(name)
autoloaded_constants.include?(name)
@@ -694,7 +717,7 @@ module ActiveSupport #:nodoc:
when String then desc.sub(/^::/, "")
when Symbol then desc.to_s
when Module
- desc.name ||
+ real_mod_name(desc) ||
raise(ArgumentError, "Anonymous modules have no name to be referenced by")
else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
end
@@ -768,6 +791,13 @@ module ActiveSupport #:nodoc:
def log(message)
logger.debug("autoloading: #{message}") if logger && verbose
end
+
+ private
+ # Returns the original name of a class or module even if `name` has been
+ # overridden.
+ def real_mod_name(mod)
+ UNBOUND_METHOD_MODULE_NAME.bind(mod).call
+ end
end
end
diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
index 821e3f971e..2cf55713bd 100644
--- a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
+++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
@@ -28,7 +28,7 @@ module ActiveSupport
end
def autoloaded?(object)
- cpath = object.is_a?(Module) ? object.name : object.to_s
+ cpath = object.is_a?(Module) ? real_mod_name(object) : object.to_s
Rails.autoloaders.main.unloadable_cpath?(cpath)
end
@@ -42,6 +42,17 @@ module ActiveSupport
end
end
+ module RequireDependency
+ def require_dependency(filename)
+ filename = filename.to_path if filename.respond_to?(:to_path)
+ if abspath = ActiveSupport::Dependencies.search_for_file(filename)
+ require abspath
+ else
+ require filename
+ end
+ end
+ end
+
module Inflector
def self.camelize(basename, _abspath)
basename.camelize
@@ -90,7 +101,7 @@ module ActiveSupport
def decorate_dependencies
Dependencies.unhook!
Dependencies.singleton_class.prepend(Decorations)
- Object.class_eval { alias_method :require_dependency, :require }
+ Object.prepend(RequireDependency)
end
end
end
diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
index 56f1e23136..d7d3c30b97 100644
--- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
@@ -120,9 +120,16 @@ module ActiveSupport
# # => DEPRECATION WARNING: PLANETS is deprecated! Use PLANETS_POST_2006 instead.
# (Backtrace information…)
# ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
- class DeprecatedConstantProxy < DeprecationProxy
+ class DeprecatedConstantProxy < Module
+ def self.new(*args, &block)
+ object = args.first
+
+ return object unless object
+ super
+ end
+
def initialize(old_const, new_const, deprecator = ActiveSupport::Deprecation.instance, message: "#{old_const} is deprecated! Use #{new_const} instead.")
- require "active_support/inflector/methods"
+ Kernel.require "active_support/inflector/methods"
@old_const = old_const
@new_const = new_const
@@ -130,6 +137,14 @@ module ActiveSupport
@message = message
end
+ instance_methods.each { |m| undef_method m unless /^__|^object_id$/.match?(m) }
+
+ # Don't give a deprecation warning on inspect since test/unit and error
+ # logs rely on it for diagnostics.
+ def inspect
+ target.inspect
+ end
+
# Returns the class of the new constant.
#
# PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune)
@@ -144,8 +159,14 @@ module ActiveSupport
ActiveSupport::Inflector.constantize(@new_const.to_s)
end
- def warn(callstack, called, args)
- @deprecator.warn(@message, callstack)
+ def const_missing(name)
+ @deprecator.warn(@message, caller_locations)
+ target.const_get(name)
+ end
+
+ def method_missing(called, *args, &block)
+ @deprecator.warn(@message, caller_locations)
+ target.__send__(called, *args, &block)
end
end
end
diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb
index 1dad4f923e..b14842bf67 100644
--- a/activesupport/lib/active_support/descendants_tracker.rb
+++ b/activesupport/lib/active_support/descendants_tracker.rb
@@ -77,15 +77,17 @@ module ActiveSupport
end
def <<(klass)
- cleanup!
@refs << WeakRef.new(klass)
end
def each
- @refs.each do |ref|
+ @refs.reject! do |ref|
yield ref.__getobj__
+ false
rescue WeakRef::RefError
+ true
end
+ self
end
def refs_size
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 5981763f0e..6acf64cb39 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -367,18 +367,20 @@ module ActiveSupport
key.kind_of?(Symbol) ? key.to_s : key
end
- def convert_value(value, options = {}) # :doc:
+ def convert_value(value, for: nil) # :doc:
+ conversion = binding.local_variable_get(:for)
+
if value.is_a? Hash
- if options[:for] == :to_hash
+ if conversion == :to_hash
value.to_hash
else
value.nested_under_indifferent_access
end
elsif value.is_a?(Array)
- if options[:for] != :assignment || value.frozen?
+ if conversion != :assignment || value.frozen?
value = value.dup
end
- value.map! { |e| convert_value(e, options) }
+ value.map! { |e| convert_value(e, for: conversion) }
else
value
end
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index 5b29a13894..efee74a1df 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -2,7 +2,6 @@
require "concurrent/map"
require "active_support/i18n"
-require "active_support/deprecation"
module ActiveSupport
module Inflector
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 18f3f53879..33a17a7741 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "active_support/inflections"
+require "active_support/core_ext/object/blank"
module ActiveSupport
# The Inflector transforms words from singular to plural, class names to table
@@ -269,32 +270,36 @@ module ActiveSupport
# NameError is raised when the name is not in CamelCase or the constant is
# unknown.
def constantize(camel_cased_word)
- names = camel_cased_word.split("::")
-
- # Trigger a built-in NameError exception including the ill-formed constant in the message.
- Object.const_get(camel_cased_word) if names.empty?
-
- # Remove the first blank element in case of '::ClassName' notation.
- names.shift if names.size > 1 && names.first.empty?
-
- names.inject(Object) do |constant, name|
- if constant == Object
- constant.const_get(name)
- else
- candidate = constant.const_get(name)
- next candidate if constant.const_defined?(name, false)
- next candidate unless Object.const_defined?(name)
-
- # Go down the ancestors to check if it is owned directly. The check
- # stops when we reach Object or the end of ancestors tree.
- constant = constant.ancestors.inject(constant) do |const, ancestor|
- break const if ancestor == Object
- break ancestor if ancestor.const_defined?(name, false)
- const
+ if camel_cased_word.blank? || !camel_cased_word.include?("::")
+ Object.const_get(camel_cased_word)
+ else
+ names = camel_cased_word.split("::")
+
+ # Trigger a built-in NameError exception including the ill-formed constant in the message.
+ Object.const_get(camel_cased_word) if names.empty?
+
+ # Remove the first blank element in case of '::ClassName' notation.
+ names.shift if names.size > 1 && names.first.empty?
+
+ names.inject(Object) do |constant, name|
+ if constant == Object
+ constant.const_get(name)
+ else
+ candidate = constant.const_get(name)
+ next candidate if constant.const_defined?(name, false)
+ next candidate unless Object.const_defined?(name)
+
+ # Go down the ancestors to check if it is owned directly. The check
+ # stops when we reach Object or the end of ancestors tree.
+ constant = constant.ancestors.inject(constant) do |const, ancestor|
+ break const if ancestor == Object
+ break ancestor if ancestor.const_defined?(name, false)
+ const
+ end
+
+ # owner is in Object, so raise
+ constant.const_get(name, false)
end
-
- # owner is in Object, so raise
- constant.const_get(name, false)
end
end
end
@@ -371,7 +376,7 @@ module ActiveSupport
last = parts.pop
- parts.reverse.inject(last) do |acc, part|
+ parts.reverse!.inject(last) do |acc, part|
part.empty? ? acc : "#{part}(::#{acc})?"
end
end
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index ec6e9ccb59..1899a1212d 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -56,14 +56,40 @@ module ActiveSupport
#
# transliterate('Jürgen', locale: :de)
# # => "Juergen"
+ #
+ # Transliteration is restricted to UTF-8, US-ASCII and GB18030 strings
+ # Other encodings will raise an ArgumentError.
def transliterate(string, replacement = "?", locale: nil)
+ string = string.dup if string.frozen?
raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String)
- I18n.transliterate(
+ allowed_encodings = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::GB18030]
+ raise ArgumentError, "Can not transliterate strings with #{string.encoding} encoding" unless allowed_encodings.include?(string.encoding)
+
+ input_encoding = string.encoding
+
+ # US-ASCII is a subset of UTF-8 so we'll force encoding as UTF-8 if
+ # US-ASCII is given. This way we can let tidy_bytes handle the string
+ # in the same way as we do for UTF-8
+ string.force_encoding(Encoding::UTF_8) if string.encoding == Encoding::US_ASCII
+
+ # GB18030 is Unicode compatible but is not a direct mapping so needs to be
+ # transcoded. Using invalid/undef :replace will result in loss of data in
+ # the event of invalid characters, but since tidy_bytes will replace
+ # invalid/undef with a "?" we're safe to do the same beforehand
+ string.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) if string.encoding == Encoding::GB18030
+
+ transliterated = I18n.transliterate(
ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc),
replacement: replacement,
locale: locale
)
+
+ # Restore the string encoding of the input if it was not UTF-8.
+ # Apply invalid/undef :replace as tidy_bytes does
+ transliterated.encode!(input_encoding, invalid: :replace, undef: :replace) if input_encoding != transliterated.encoding
+
+ transliterated
end
# Replaces special characters in a string so that it may be used as part of
@@ -91,7 +117,7 @@ module ActiveSupport
# If the optional parameter +locale+ is specified,
# the word will be parameterized as a word of that language.
# By default, this parameter is set to <tt>nil</tt> and it will use
- # the configured <tt>I18n.locale<tt>.
+ # the configured <tt>I18n.locale</tt>.
def parameterize(string, separator: "-", preserve_case: false, locale: nil)
# Replace accented chars with their ASCII equivalents.
parameterized_string = transliterate(string, locale: locale)
diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb
index db991c7a32..8b9dd1fffe 100644
--- a/activesupport/lib/active_support/log_subscriber.rb
+++ b/activesupport/lib/active_support/log_subscriber.rb
@@ -29,6 +29,9 @@ module ActiveSupport
# subscriber, the line above should be called after your
# <tt>ActiveRecord::LogSubscriber</tt> definition.
#
+ # A logger also needs to be set with <tt>ActiveRecord::LogSubscriber.logger=</tt>.
+ # This is assigned automatically in a Rails environment.
+ #
# After configured, whenever a <tt>"sql.active_record"</tt> notification is published,
# it will properly dispatch the event
# (<tt>ActiveSupport::Notifications::Event</tt>) to the sql method.
diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb
index b8555c887b..2576bee4ff 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -14,7 +14,7 @@ module ActiveSupport
# ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT)
# # => true
def self.logger_outputs_to?(logger, *sources)
- logdev = logger.instance_variable_get("@logdev")
+ logdev = logger.instance_variable_get(:@logdev)
logger_source = logdev.dev if logdev.respond_to?(:dev)
sources.any? { |source| source == logger_source }
end
diff --git a/activesupport/lib/active_support/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb
index f16c90cfc6..1775a41492 100644
--- a/activesupport/lib/active_support/logger_thread_safe_level.rb
+++ b/activesupport/lib/active_support/logger_thread_safe_level.rb
@@ -3,6 +3,7 @@
require "active_support/concern"
require "active_support/core_ext/module/attribute_accessors"
require "concurrent"
+require "fiber"
module ActiveSupport
module LoggerThreadSafeLevel # :nodoc:
@@ -28,7 +29,7 @@ module ActiveSupport
end
def local_log_id
- Thread.current.__id__
+ Fiber.current.__id__
end
def local_level
diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb
index c4a4afe95f..a5dc1181d8 100644
--- a/activesupport/lib/active_support/message_verifier.rb
+++ b/activesupport/lib/active_support/message_verifier.rb
@@ -178,8 +178,8 @@ module ActiveSupport
# Generates a signed message for the provided value.
#
- # The message is signed with the +MessageVerifier+'s secret. Without knowing
- # the secret, the original value cannot be extracted from the message.
+ # The message is signed with the +MessageVerifier+'s secret.
+ # Returns Base64-encoded message joined with the generated signature.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index 2ba3936cae..868cb70423 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -48,7 +48,7 @@ module ActiveSupport #:nodoc:
alias to_s wrapped_string
alias to_str wrapped_string
- delegate :<=>, :=~, :acts_like_string?, to: :wrapped_string
+ delegate :<=>, :=~, :match?, :acts_like_string?, to: :wrapped_string
# Creates a new Chars instance by wrapping _string_.
def initialize(string)
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index aa602329ec..b0f30d2995 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -3,6 +3,7 @@
require "mutex_m"
require "concurrent/map"
require "set"
+require "active_support/core_ext/object/try"
module ActiveSupport
module Notifications
@@ -218,6 +219,7 @@ module ActiveSupport
def finish(name, id, payload)
stack = Thread.current[:_event_stack]
event = stack.pop
+ event.payload = payload
event.finish!
@delegate.call event
end
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index 7ab39c9bfb..24e1ab313a 100644
--- a/activesupport/lib/active_support/notifications/instrumenter.rb
+++ b/activesupport/lib/active_support/notifications/instrumenter.rb
@@ -52,7 +52,8 @@ module ActiveSupport
end
class Event
- attr_reader :name, :time, :end, :transaction_id, :payload, :children
+ attr_reader :name, :time, :end, :transaction_id, :children
+ attr_accessor :payload
def self.clock_gettime_supported? # :nodoc:
defined?(Process::CLOCK_PROCESS_CPUTIME_ID) &&
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 8f3d04aa5c..0c87114c0d 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/dependencies/autoload"
-
module ActiveSupport
module NumberHelper
extend ActiveSupport::Autoload
diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb
index ab9ca727f6..8e54b11be6 100644
--- a/activesupport/lib/active_support/option_merger.rb
+++ b/activesupport/lib/active_support/option_merger.rb
@@ -5,7 +5,7 @@ require "active_support/core_ext/hash/deep_merge"
module ActiveSupport
class OptionMerger #:nodoc:
instance_methods.each do |method|
- undef_method(method) if method !~ /^(__|instance_eval|class|object_id)/
+ undef_method(method) unless method.match?(/^(__|instance_eval|class|object_id)/)
end
def initialize(context, options)
diff --git a/activesupport/lib/active_support/parameter_filter.rb b/activesupport/lib/active_support/parameter_filter.rb
index 8e5595babf..f4c4f2d2fb 100644
--- a/activesupport/lib/active_support/parameter_filter.rb
+++ b/activesupport/lib/active_support/parameter_filter.rb
@@ -22,7 +22,7 @@ module ActiveSupport
# change { file: { code: "xxxx"} }
#
# ActiveSupport::ParameterFilter.new([-> (k, v) do
- # v.reverse! if k =~ /secret/i
+ # v.reverse! if /secret/i.match?(k)
# end])
# => reverses the value to all keys matching /secret/i
class ParameterFilter
@@ -109,7 +109,12 @@ module ActiveSupport
elsif value.is_a?(Hash)
value = call(value, parents, original_params)
elsif value.is_a?(Array)
- value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v }
+ # If we don't pop the current parent it will be duplicated as we
+ # process each array value.
+ parents.pop if deep_regexps
+ value = value.map { |v| value_for_key(key, v, parents, original_params) }
+ # Restore the parent stack after processing the array.
+ parents.push(key) if deep_regexps
elsif blocks.any?
key = key.dup if key.duplicable?
value = value.dup if value.duplicable?
diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb
index eb92fb4371..bf5a3b97ae 100644
--- a/activesupport/lib/active_support/per_thread_registry.rb
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -40,7 +40,7 @@ module ActiveSupport
# If the class has an initializer, it must accept no arguments.
module PerThreadRegistry
def self.extended(object)
- object.instance_variable_set "@per_thread_registry_key", object.name.freeze
+ object.instance_variable_set :@per_thread_registry_key, object.name.freeze
end
def instance
diff --git a/activesupport/lib/active_support/rails.rb b/activesupport/lib/active_support/rails.rb
index 8b727a69ec..30857f04d8 100644
--- a/activesupport/lib/active_support/rails.rb
+++ b/activesupport/lib/active_support/rails.rb
@@ -13,9 +13,6 @@
# Defines Object#blank? and Object#present?.
require "active_support/core_ext/object/blank"
-# Rails own autoload, eager_load, etc.
-require "active_support/dependencies/autoload"
-
# Support for ClassMethods and the included macro.
require "active_support/concern"
diff --git a/activesupport/lib/active_support/secure_compare_rotator.rb b/activesupport/lib/active_support/secure_compare_rotator.rb
new file mode 100644
index 0000000000..269703c34a
--- /dev/null
+++ b/activesupport/lib/active_support/secure_compare_rotator.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require "active_support/security_utils"
+require "active_support/messages/rotator"
+
+module ActiveSupport
+ # The ActiveSupport::SecureCompareRotator is a wrapper around +ActiveSupport::SecurityUtils.secure_compare+
+ # and allows you to rotate a previously defined value to a new one.
+ #
+ # It can be used as follow:
+ #
+ # rotator = ActiveSupport::SecureCompareRotator.new('new_production_value')
+ # rotator.rotate('previous_production_value')
+ # rotator.secure_compare!('previous_production_value')
+ #
+ # One real use case example would be to rotate a basic auth credentials:
+ #
+ # class MyController < ApplicationController
+ # def authenticate_request
+ # rotator = ActiveSupport::SecureComparerotator.new('new_password')
+ # rotator.rotate('old_password')
+ #
+ # authenticate_or_request_with_http_basic do |username, password|
+ # rotator.secure_compare!(password)
+ # rescue ActiveSupport::SecureCompareRotator::InvalidMatch
+ # false
+ # end
+ # end
+ # end
+ class SecureCompareRotator
+ include SecurityUtils
+ prepend Messages::Rotator
+
+ InvalidMatch = Class.new(StandardError)
+
+ def initialize(value, **_options)
+ @value = value
+ end
+
+ def secure_compare!(other_value, on_rotation: @on_rotation)
+ secure_compare(@value, other_value) ||
+ run_rotations(on_rotation) { |wrapper| wrapper.secure_compare!(other_value) } ||
+ raise(InvalidMatch)
+ end
+
+ private
+ def build_rotation(previous_value, _options)
+ self.class.new(previous_value)
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb
index d8a86d997e..75a7ca24c2 100644
--- a/activesupport/lib/active_support/tagged_logging.rb
+++ b/activesupport/lib/active_support/tagged_logging.rb
@@ -32,15 +32,18 @@ module ActiveSupport
def push_tags(*tags)
tags.flatten.reject(&:blank?).tap do |new_tags|
+ @tags_text = nil
current_tags.concat new_tags
end
end
def pop_tags(size = 1)
+ @tags_text = nil
current_tags.pop size
end
def clear_tags!
+ @tags_text = nil
current_tags.clear
end
@@ -51,11 +54,13 @@ module ActiveSupport
end
def tags_text
- tags = current_tags
- if tags.one?
- "[#{tags[0]}] "
- elsif tags.any?
- tags.collect { |tag| "[#{tag}] " }.join
+ @tags_text ||= begin
+ tags = current_tags
+ if tags.one?
+ "[#{tags[0]}] "
+ elsif tags.any?
+ tags.collect { |tag| "[#{tag}] " }.join
+ end
end
end
end
diff --git a/activesupport/lib/active_support/testing/parallelization.rb b/activesupport/lib/active_support/testing/parallelization.rb
index f50a5e0554..96518a4a58 100644
--- a/activesupport/lib/active_support/testing/parallelization.rb
+++ b/activesupport/lib/active_support/testing/parallelization.rb
@@ -72,7 +72,11 @@ module ActiveSupport
def start
@pool = @queue_size.times.map do |worker|
+ title = "Rails test worker #{worker}"
+
fork do
+ Process.setproctitle("#{title} - (starting)")
+
DRb.stop_service
begin
@@ -85,6 +89,9 @@ module ActiveSupport
klass = job[0]
method = job[1]
reporter = job[2]
+
+ Process.setproctitle("#{title} - #{klass}##{method}")
+
result = klass.with_info_handler reporter do
Minitest.run_one_method(klass, method)
end
@@ -99,8 +106,12 @@ module ActiveSupport
end
queue.record(reporter, result)
end
+
+ Process.setproctitle("#{title} - (idle)")
end
ensure
+ Process.setproctitle("#{title} - (stopping)")
+
run_cleanup(worker)
end
end
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index d9e033e23b..90830b89bd 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -334,6 +334,13 @@ module ActiveSupport
re === name || re === MAPPING[name]
end
+ # Compare #name and TZInfo identifier to a supplied regexp, returning +true+
+ # if a match is found.
+ def match?(re)
+ (re == name) || (re == MAPPING[name]) ||
+ ((Regexp === re) && (re.match?(name) || re.match?(MAPPING[name])))
+ end
+
# Returns a textual representation of this time zone.
def to_s
"(GMT#{formatted_offset}) #{name}"
diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb
index baa38ba6ac..6f5d53c190 100644
--- a/activesupport/test/cache/behaviors/local_cache_behavior.rb
+++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb
@@ -46,6 +46,15 @@ module LocalCacheBehavior
end
end
+ def test_local_cache_of_read_returns_a_copy_of_the_entry
+ @cache.with_local_cache do
+ @cache.write(:foo, type: "bar")
+ value = @cache.read(:foo)
+ assert_equal("bar", value.delete(:type))
+ assert_equal({ type: "bar" }, @cache.read(:foo))
+ end
+ end
+
def test_local_cache_of_read
@cache.write("foo", "bar")
@cache.with_local_cache do
diff --git a/activesupport/test/cache/stores/mem_cache_store_test.rb b/activesupport/test/cache/stores/mem_cache_store_test.rb
index 3917d14d1c..0a5e20ed46 100644
--- a/activesupport/test/cache/stores/mem_cache_store_test.rb
+++ b/activesupport/test/cache/stores/mem_cache_store_test.rb
@@ -9,7 +9,7 @@ require "dalli"
# connection pool testing.
class SlowDalliClient < Dalli::Client
def get(key, options = {})
- if key =~ /latency/
+ if /latency/.match?(key)
sleep 3
else
super
diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb
index a2177e0476..0ea37b15d5 100644
--- a/activesupport/test/cache/stores/redis_cache_store_test.rb
+++ b/activesupport/test/cache/stores/redis_cache_store_test.rb
@@ -15,7 +15,7 @@ Redis::Connection.drivers.append(driver)
# connection pool testing.
class SlowRedis < Redis
def get(key, options = {})
- if key =~ /latency/
+ if /latency/.match?(key)
sleep 3
else
super
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index cc37c4fa99..221a8a5731 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -256,7 +256,7 @@ module CallbacksTest
end
def respond_to_missing?(sym)
- sym =~ /^(log|wrap)_/ || super
+ sym.match?(/^(log|wrap)_/) || super
end
end
diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb
index 381b5a1f32..a9bf4b82f4 100644
--- a/activesupport/test/core_ext/enumerable_test.rb
+++ b/activesupport/test/core_ext/enumerable_test.rb
@@ -242,4 +242,28 @@ class EnumerableTests < ActiveSupport::TestCase
])
assert_equal [[5, 99], [15, 0], [10, 50]], payments.pluck(:dollars, :cents)
end
+
+ def test_compact_blank
+ values = GenericEnumerable.new([1, "", nil, 2, " ", [], {}, false, true])
+
+ assert_equal [1, 2, true], values.compact_blank
+ end
+
+ def test_array_compact_blank!
+ values = [1, "", nil, 2, " ", [], {}, false, true]
+ values.compact_blank!
+
+ assert_equal [1, 2, true], values
+ end
+
+ def test_hash_compact_blank
+ values = { a: "", b: 1, c: nil, d: [], e: false, f: true }
+ assert_equal({ b: 1, f: true }, values.compact_blank)
+ end
+
+ def test_hash_compact_blank!
+ values = { a: "", b: 1, c: nil, d: [], e: false, f: true }
+ values.compact_blank!
+ assert_equal({ b: 1, f: true }, values)
+ end
end
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index 8572d56722..59d2d6f530 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -6,6 +6,7 @@ require "bigdecimal"
require "active_support/core_ext/string/access"
require "active_support/ordered_hash"
require "active_support/core_ext/object/conversions"
+require "active_support/core_ext/date/conversions"
require "active_support/core_ext/object/deep_dup"
require "active_support/inflections"
diff --git a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
index e0e331fc91..a2b3239884 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
@@ -4,23 +4,32 @@ require "abstract_unit"
require "active_support/core_ext/module/attribute_accessors_per_thread"
class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
- def setup
- @class = Class.new do
- thread_mattr_accessor :foo
- thread_mattr_accessor :bar, instance_writer: false
- thread_mattr_reader :shaq, instance_reader: false
- thread_mattr_accessor :camp, instance_accessor: false
-
- def self.name; "MyClass" end
- end
+ class MyClass
+ thread_mattr_accessor :foo
+ thread_mattr_accessor :bar, instance_writer: false
+ thread_mattr_reader :shaq, instance_reader: false
+ thread_mattr_accessor :camp, instance_accessor: false
+ end
- @subclass = Class.new(@class) do
- def self.name; "SubMyClass" end
- end
+ class SubMyClass < MyClass
+ end
+ setup do
+ @class = MyClass
+ @subclass = SubMyClass
@object = @class.new
end
+ def test_can_initialize_with_default_value
+ Thread.new do
+ @class.thread_mattr_accessor :baz, default: "default_value"
+
+ assert_equal "default_value", @class.baz
+ end.join
+
+ assert_nil @class.baz
+ end
+
def test_should_use_mattr_default
Thread.new do
assert_nil @class.foo
@@ -66,23 +75,23 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
threads = []
threads << Thread.new do
@class.foo = "things"
- sleep 1
+ Thread.pass
assert_equal "things", @class.foo
end
threads << Thread.new do
@class.foo = "other things"
- sleep 1
+ Thread.pass
assert_equal "other things", @class.foo
end
threads << Thread.new do
@class.foo = "really other things"
- sleep 1
+ Thread.pass
assert_equal "really other things", @class.foo
end
- threads.each { |t| t.join }
+ threads.each(&:join)
end
def test_should_raise_name_error_if_attribute_name_is_invalid
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index ec9ecd06ee..dd36a9373a 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -111,6 +111,24 @@ class DecoratedReserved
end
end
+class Maze
+ attr_accessor :cavern, :passages
+end
+
+class Cavern
+ delegate_missing_to :target
+
+ attr_reader :maze
+
+ def initialize(maze)
+ @maze = maze
+ end
+
+ def target
+ @maze.passages = :twisty
+ end
+end
+
class Block
def hello?
true
@@ -411,6 +429,17 @@ class ModuleTest < ActiveSupport::TestCase
assert_respond_to DecoratedTester.new(@david), :extra_missing
end
+ def test_delegate_missing_to_does_not_interfere_with_marshallization
+ maze = Maze.new
+ maze.cavern = Cavern.new(maze)
+
+ array = [maze, nil]
+ serialized_array = Marshal.dump(array)
+ deserialized_array = Marshal.load(serialized_array)
+
+ assert_nil deserialized_array[1]
+ end
+
def test_delegate_with_case
event = Event.new(Tester.new)
assert_equal 1, event.foo
diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb
index 31241caf0a..8aa9eb036a 100644
--- a/activesupport/test/core_ext/object/acts_like_test.rb
+++ b/activesupport/test/core_ext/object/acts_like_test.rb
@@ -1,7 +1,10 @@
# frozen_string_literal: true
require "abstract_unit"
-require "active_support/core_ext/object"
+require "active_support/core_ext/date/acts_like"
+require "active_support/core_ext/time/acts_like"
+require "active_support/core_ext/date_time/acts_like"
+require "active_support/core_ext/object/acts_like"
class ObjectTests < ActiveSupport::TestCase
class DuckTime
diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb
index 1fb26ebac7..8e5e28c513 100644
--- a/activesupport/test/core_ext/object/deep_dup_test.rb
+++ b/activesupport/test/core_ext/object/deep_dup_test.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "abstract_unit"
-require "active_support/core_ext/object"
+require "active_support/core_ext/object/deep_dup"
class DeepDupTest < ActiveSupport::TestCase
def test_array_deep_dup
diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb
index c9af2cb624..a577c30c40 100644
--- a/activesupport/test/core_ext/object/duplicable_test.rb
+++ b/activesupport/test/core_ext/object/duplicable_test.rb
@@ -6,7 +6,7 @@ require "active_support/core_ext/object/duplicable"
require "active_support/core_ext/numeric/time"
class DuplicableTest < ActiveSupport::TestCase
- RAISE_DUP = [method(:puts)]
+ RAISE_DUP = [method(:puts), method(:puts).unbind]
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)]
def test_duplicable
diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb
index 9052d209a3..dd710e9349 100644
--- a/activesupport/test/core_ext/object/instance_variables_test.rb
+++ b/activesupport/test/core_ext/object/instance_variables_test.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "abstract_unit"
-require "active_support/core_ext/object"
+require "active_support/core_ext/object/instance_variables"
class ObjectInstanceVariableTest < ActiveSupport::TestCase
def setup
diff --git a/activesupport/test/core_ext/object/json_gem_encoding_test.rb b/activesupport/test/core_ext/object/json_gem_encoding_test.rb
index 4cdb6ed09f..eef02f7458 100644
--- a/activesupport/test/core_ext/object/json_gem_encoding_test.rb
+++ b/activesupport/test/core_ext/object/json_gem_encoding_test.rb
@@ -22,7 +22,7 @@ class JsonGemEncodingTest < ActiveSupport::TestCase
JSONTest::EncodingTestCases.constants.each_with_index do |name|
JSONTest::EncodingTestCases.const_get(name).each_with_index do |(subject, _), i|
- test("#{name[0..-6].underscore} #{i}") do
+ test("#{name[0..-6]} #{i}") do
assert_same_with_or_without_active_support(subject)
end
end
diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb
index a838334034..d501b5edce 100644
--- a/activesupport/test/core_ext/object/try_test.rb
+++ b/activesupport/test/core_ext/object/try_test.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "abstract_unit"
-require "active_support/core_ext/object"
+require "active_support/core_ext/object/try"
class ObjectTryTest < ActiveSupport::TestCase
def setup
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index c5a000b67a..af8f9c9b09 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -455,6 +455,8 @@ class StringAccessTest < ActiveSupport::TestCase
test "#to with negative Integer, position is counted from the end" do
assert_equal "hell", "hello".to(-2)
+ assert_equal "h", "hello".to(-5)
+ assert_equal "", "hello".to(-7)
end
test "#from and #to can be combined" do
@@ -480,12 +482,16 @@ class StringAccessTest < ActiveSupport::TestCase
assert_not_same different_string, string
end
- test "#first with negative Integer is deprecated" do
- string = "hello"
- message = "Calling String#first with a negative integer limit " \
- "will raise an ArgumentError in Rails 6.1."
- assert_deprecated(message) do
- string.first(-1)
+ test "#first with Integer returns a non-frozen string" do
+ string = "he"
+ (0..string.length + 1).each do |limit|
+ assert_not string.first(limit).frozen?
+ end
+ end
+
+ test "#first with negative Integer raises ArgumentError" do
+ assert_raise ArgumentError do
+ "hello".first(-1)
end
end
@@ -507,12 +513,16 @@ class StringAccessTest < ActiveSupport::TestCase
assert_not_same different_string, string
end
- test "#last with negative Integer is deprecated" do
- string = "hello"
- message = "Calling String#last with a negative integer limit " \
- "will raise an ArgumentError in Rails 6.1."
- assert_deprecated(message) do
- string.last(-1)
+ test "#last with Integer returns a non-frozen string" do
+ string = "he"
+ (0..string.length + 1).each do |limit|
+ assert_not string.last(limit).frozen?
+ end
+ end
+
+ test "#last with negative Integer raises ArgumentError" do
+ assert_raise ArgumentError do
+ "hello".last(-1)
end
end
diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb
index adbdc646bc..4cbf462da0 100644
--- a/activesupport/test/current_attributes_test.rb
+++ b/activesupport/test/current_attributes_test.rb
@@ -9,7 +9,7 @@ class CurrentAttributesTest < ActiveSupport::TestCase
attribute :world, :account, :person, :request
delegate :time_zone, to: :person
- before_reset { Session.previous = person.try(:id) }
+ before_reset { Session.previous = person&.id }
resets do
Time.zone = "UTC"
@@ -18,13 +18,13 @@ class CurrentAttributesTest < ActiveSupport::TestCase
def account=(account)
super
- self.person = "#{account}'s person"
+ self.person = Person.new(1, "#{account}'s person")
end
def person=(person)
super
- Time.zone = person.try(:time_zone)
- Session.current = person.try(:id)
+ Time.zone = person&.time_zone
+ Session.current = person&.id
end
def request
@@ -63,7 +63,7 @@ class CurrentAttributesTest < ActiveSupport::TestCase
test "set attribute via overwritten method" do
Current.account = "account/1"
assert_equal "account/1", Current.account
- assert_equal "account/1's person", Current.person
+ assert_equal "account/1's person", Current.person.name
end
test "set auxiliary class via overwritten method" do
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index 003a0dbccb..6bad69f7f2 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -592,6 +592,13 @@ class DependenciesTest < ActiveSupport::TestCase
nil_name = Module.new
def nil_name.name() nil end
assert_not ActiveSupport::Dependencies.autoloaded?(nil_name)
+
+ invalid_constant_name = Module.new do
+ def self.name
+ "primary::SchemaMigration"
+ end
+ end
+ assert_not ActiveSupport::Dependencies.autoloaded?(invalid_constant_name)
end
ensure
remove_constants(:ModuleFolder)
diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb
index f25c704586..ae2f4a6e58 100644
--- a/activesupport/test/deprecation_test.rb
+++ b/activesupport/test/deprecation_test.rb
@@ -38,6 +38,11 @@ class Deprecatee
C = 1
end
A = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Deprecatee::A", "Deprecatee::B::C")
+
+ module New
+ class Descendant; end
+ end
+ Old = ActiveSupport::Deprecation::DeprecatedConstantProxy.new("Deprecatee::Old", "Deprecatee::New")
end
class DeprecateeWithAccessor
@@ -210,6 +215,18 @@ class DeprecationTest < ActiveSupport::TestCase
assert_not_deprecated { assert_equal Deprecatee::B::C.class, Deprecatee::A.class }
end
+ def test_deprecated_constant_descendant
+ assert_not_deprecated { Deprecatee::New::Descendant }
+
+ assert_deprecated("Deprecatee::Old") do
+ assert_equal Deprecatee::Old::Descendant, Deprecatee::New::Descendant
+ end
+
+ assert_raises(NameError) do
+ assert_deprecated("Deprecatee::Old") { Deprecatee::Old::NON_EXISTENCE }
+ end
+ end
+
def test_deprecated_constant_accessor
assert_not_deprecated { DeprecateeWithAccessor::B::C }
assert_deprecated("DeprecateeWithAccessor::A") { assert_equal DeprecateeWithAccessor::B::C, DeprecateeWithAccessor::A }
diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb
index 84d89fa0a7..bd056a1576 100644
--- a/activesupport/test/file_update_checker_shared_tests.rb
+++ b/activesupport/test/file_update_checker_shared_tests.rb
@@ -3,294 +3,298 @@
require "fileutils"
module FileUpdateCheckerSharedTests
- extend ActiveSupport::Testing::Declarative
- include FileUtils
+ def self.included(kls)
+ kls.class_eval do
+ extend ActiveSupport::Testing::Declarative
+ include FileUtils
- def tmpdir
- @tmpdir
- end
+ def tmpdir
+ @tmpdir
+ end
- def tmpfile(name)
- File.join(tmpdir, name)
- end
+ def tmpfile(name)
+ File.join(tmpdir, name)
+ end
- def tmpfiles
- @tmpfiles ||= %w(foo.rb bar.rb baz.rb).map { |f| tmpfile(f) }
- end
+ def tmpfiles
+ @tmpfiles ||= %w(foo.rb bar.rb baz.rb).map { |f| tmpfile(f) }
+ end
- def run(*args)
- capture_exceptions do
- Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super }
- end
- end
+ def run(*args)
+ capture_exceptions do
+ Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super }
+ end
+ end
- test "should not execute the block if no paths are given" do
- silence_warnings { require "listen" }
- i = 0
+ test "should not execute the block if no paths are given" do
+ silence_warnings { require "listen" }
+ i = 0
- checker = new_checker { i += 1 }
+ checker = new_checker { i += 1 }
- assert_not checker.execute_if_updated
- assert_equal 0, i
- end
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
+ end
- test "should not execute the block if no files change" do
- i = 0
+ test "should not execute the block if no files change" do
+ i = 0
- FileUtils.touch(tmpfiles)
+ FileUtils.touch(tmpfiles)
- checker = new_checker(tmpfiles) { i += 1 }
+ checker = new_checker(tmpfiles) { i += 1 }
- assert_not checker.execute_if_updated
- assert_equal 0, i
- end
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
+ end
- test "should execute the block once when files are created" do
- i = 0
+ test "should execute the block once when files are created" do
+ i = 0
- checker = new_checker(tmpfiles) { i += 1 }
+ checker = new_checker(tmpfiles) { i += 1 }
- touch(tmpfiles)
- wait
+ touch(tmpfiles)
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "should execute the block once when files are modified" do
- i = 0
+ test "should execute the block once when files are modified" do
+ i = 0
- FileUtils.touch(tmpfiles)
+ FileUtils.touch(tmpfiles)
- checker = new_checker(tmpfiles) { i += 1 }
+ checker = new_checker(tmpfiles) { i += 1 }
- touch(tmpfiles)
- wait
+ touch(tmpfiles)
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "should execute the block once when files are deleted" do
- i = 0
+ test "should execute the block once when files are deleted" do
+ i = 0
- FileUtils.touch(tmpfiles)
+ FileUtils.touch(tmpfiles)
- checker = new_checker(tmpfiles) { i += 1 }
+ checker = new_checker(tmpfiles) { i += 1 }
- rm_f(tmpfiles)
- wait
+ rm_f(tmpfiles)
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "updated should become true when watched files are created" do
- i = 0
+ test "updated should become true when watched files are created" do
+ i = 0
- checker = new_checker(tmpfiles) { i += 1 }
- assert_not_predicate checker, :updated?
+ checker = new_checker(tmpfiles) { i += 1 }
+ assert_not_predicate checker, :updated?
- touch(tmpfiles)
- wait
+ touch(tmpfiles)
+ wait
- assert_predicate checker, :updated?
- end
+ assert_predicate checker, :updated?
+ end
- test "updated should become true when watched files are modified" do
- i = 0
+ test "updated should become true when watched files are modified" do
+ i = 0
- FileUtils.touch(tmpfiles)
+ FileUtils.touch(tmpfiles)
- checker = new_checker(tmpfiles) { i += 1 }
- assert_not_predicate checker, :updated?
+ checker = new_checker(tmpfiles) { i += 1 }
+ assert_not_predicate checker, :updated?
- touch(tmpfiles)
- wait
+ touch(tmpfiles)
+ wait
- assert_predicate checker, :updated?
- end
+ assert_predicate checker, :updated?
+ end
- test "updated should become true when watched files are deleted" do
- i = 0
+ test "updated should become true when watched files are deleted" do
+ i = 0
- FileUtils.touch(tmpfiles)
+ FileUtils.touch(tmpfiles)
- checker = new_checker(tmpfiles) { i += 1 }
- assert_not_predicate checker, :updated?
+ checker = new_checker(tmpfiles) { i += 1 }
+ assert_not_predicate checker, :updated?
- rm_f(tmpfiles)
- wait
+ rm_f(tmpfiles)
+ wait
- assert_predicate checker, :updated?
- end
+ assert_predicate checker, :updated?
+ end
- test "should be robust to handle files with wrong modified time" do
- i = 0
+ test "should be robust to handle files with wrong modified time" do
+ i = 0
- FileUtils.touch(tmpfiles)
+ FileUtils.touch(tmpfiles)
- now = Time.now
- time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future
- File.utime(time, time, tmpfiles[0])
+ now = Time.now
+ time = Time.mktime(now.year + 1, now.month, now.day) # wrong mtime from the future
+ File.utime(time, time, tmpfiles[0])
- checker = new_checker(tmpfiles) { i += 1 }
+ checker = new_checker(tmpfiles) { i += 1 }
- touch(tmpfiles[1..-1])
- wait
+ touch(tmpfiles[1..-1])
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "should return max_time for files with mtime = Time.at(0)" do
- i = 0
+ test "should return max_time for files with mtime = Time.at(0)" do
+ i = 0
- FileUtils.touch(tmpfiles)
+ FileUtils.touch(tmpfiles)
- time = Time.at(0) # wrong mtime from the future
- File.utime(time, time, tmpfiles[0])
+ time = Time.at(0) # wrong mtime from the future
+ File.utime(time, time, tmpfiles[0])
- checker = new_checker(tmpfiles) { i += 1 }
+ checker = new_checker(tmpfiles) { i += 1 }
- touch(tmpfiles[1..-1])
- wait
+ touch(tmpfiles[1..-1])
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "should cache updated result until execute" do
- i = 0
+ test "should cache updated result until execute" do
+ i = 0
- checker = new_checker(tmpfiles) { i += 1 }
- assert_not_predicate checker, :updated?
+ checker = new_checker(tmpfiles) { i += 1 }
+ assert_not_predicate checker, :updated?
- touch(tmpfiles)
- wait
+ touch(tmpfiles)
+ wait
- assert_predicate checker, :updated?
- checker.execute
- assert_not_predicate checker, :updated?
- end
+ assert_predicate checker, :updated?
+ checker.execute
+ assert_not_predicate checker, :updated?
+ end
- test "should execute the block if files change in a watched directory one extension" do
- i = 0
+ test "should execute the block if files change in a watched directory one extension" do
+ i = 0
- checker = new_checker([], tmpdir => :rb) { i += 1 }
+ checker = new_checker([], tmpdir => :rb) { i += 1 }
- touch(tmpfile("foo.rb"))
- wait
+ touch(tmpfile("foo.rb"))
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "should execute the block if files change in a watched directory any extensions" do
- i = 0
+ test "should execute the block if files change in a watched directory any extensions" do
+ i = 0
- checker = new_checker([], tmpdir => []) { i += 1 }
+ checker = new_checker([], tmpdir => []) { i += 1 }
- touch(tmpfile("foo.rb"))
- wait
+ touch(tmpfile("foo.rb"))
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "should execute the block if files change in a watched directory several extensions" do
- i = 0
+ test "should execute the block if files change in a watched directory several extensions" do
+ i = 0
- checker = new_checker([], tmpdir => [:rb, :txt]) { i += 1 }
+ checker = new_checker([], tmpdir => [:rb, :txt]) { i += 1 }
- touch(tmpfile("foo.rb"))
- wait
+ touch(tmpfile("foo.rb"))
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
+ assert checker.execute_if_updated
+ assert_equal 1, i
- touch(tmpfile("foo.txt"))
- wait
+ touch(tmpfile("foo.txt"))
+ wait
- assert checker.execute_if_updated
- assert_equal 2, i
- end
+ assert checker.execute_if_updated
+ assert_equal 2, i
+ end
- test "should not execute the block if the file extension is not watched" do
- i = 0
+ test "should not execute the block if the file extension is not watched" do
+ i = 0
- checker = new_checker([], tmpdir => :txt) { i += 1 }
+ checker = new_checker([], tmpdir => :txt) { i += 1 }
- touch(tmpfile("foo.rb"))
- wait
+ touch(tmpfile("foo.rb"))
+ wait
- assert_not checker.execute_if_updated
- assert_equal 0, i
- end
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
+ end
- test "does not assume files exist on instantiation" do
- i = 0
+ test "does not assume files exist on instantiation" do
+ i = 0
- non_existing = tmpfile("non_existing.rb")
- checker = new_checker([non_existing]) { i += 1 }
+ non_existing = tmpfile("non_existing.rb")
+ checker = new_checker([non_existing]) { i += 1 }
- touch(non_existing)
- wait
+ touch(non_existing)
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "detects files in new subdirectories" do
- i = 0
+ test "detects files in new subdirectories" do
+ i = 0
- checker = new_checker([], tmpdir => :rb) { i += 1 }
+ checker = new_checker([], tmpdir => :rb) { i += 1 }
- subdir = tmpfile("subdir")
- mkdir(subdir)
- wait
+ subdir = tmpfile("subdir")
+ mkdir(subdir)
+ wait
- assert_not checker.execute_if_updated
- assert_equal 0, i
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
- touch(File.join(subdir, "nested.rb"))
- wait
+ touch(File.join(subdir, "nested.rb"))
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
- end
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
- test "looked up extensions are inherited in subdirectories not listening to them" do
- i = 0
+ test "looked up extensions are inherited in subdirectories not listening to them" do
+ i = 0
- subdir = tmpfile("subdir")
- mkdir(subdir)
+ subdir = tmpfile("subdir")
+ mkdir(subdir)
- checker = new_checker([], tmpdir => :rb, subdir => :txt) { i += 1 }
+ checker = new_checker([], tmpdir => :rb, subdir => :txt) { i += 1 }
- touch(tmpfile("new.txt"))
- wait
+ touch(tmpfile("new.txt"))
+ wait
- assert_not checker.execute_if_updated
- assert_equal 0, i
+ assert_not checker.execute_if_updated
+ assert_equal 0, i
- # subdir does not look for Ruby files, but its parent tmpdir does.
- touch(File.join(subdir, "nested.rb"))
- wait
+ # subdir does not look for Ruby files, but its parent tmpdir does.
+ touch(File.join(subdir, "nested.rb"))
+ wait
- assert checker.execute_if_updated
- assert_equal 1, i
+ assert checker.execute_if_updated
+ assert_equal 1, i
- touch(File.join(subdir, "nested.txt"))
- wait
+ touch(File.join(subdir, "nested.txt"))
+ wait
- assert checker.execute_if_updated
- assert_equal 2, i
- end
+ assert checker.execute_if_updated
+ assert_equal 2, i
+ end
- test "initialize raises an ArgumentError if no block given" do
- assert_raise ArgumentError do
- new_checker([])
+ test "initialize raises an ArgumentError if no block given" do
+ assert_raise ArgumentError do
+ new_checker([])
+ end
+ end
end
end
end
diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb
index 160e1156b6..6f7a186022 100644
--- a/activesupport/test/logger_test.rb
+++ b/activesupport/test/logger_test.rb
@@ -258,6 +258,50 @@ class LoggerTest < ActiveSupport::TestCase
assert_level(Logger::INFO)
end
+ def test_logger_level_main_fiber_safety
+ @logger.level = Logger::INFO
+ assert_level(Logger::INFO)
+
+ fiber = Fiber.new do
+ assert_level(Logger::INFO)
+ end
+
+ @logger.silence(Logger::ERROR) do
+ assert_level(Logger::ERROR)
+ fiber.resume
+ end
+ end
+
+ def test_logger_level_local_fiber_safety
+ @logger.level = Logger::INFO
+ assert_level(Logger::INFO)
+
+ another_fiber = Fiber.new do
+ @logger.silence(Logger::ERROR) do
+ assert_level(Logger::ERROR)
+ @logger.silence(Logger::DEBUG) do
+ assert_level(Logger::DEBUG)
+ end
+ end
+
+ assert_level(Logger::INFO)
+ end
+
+ Fiber.new do
+ @logger.silence(Logger::ERROR) do
+ assert_level(Logger::ERROR)
+ @logger.silence(Logger::DEBUG) do
+ another_fiber.resume
+ assert_level(Logger::DEBUG)
+ end
+ end
+
+ assert_level(Logger::INFO)
+ end.resume
+
+ assert_level(Logger::INFO)
+ end
+
private
def level_name(level)
::Logger::Severity.constants.find do |severity|
diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb
index 097aa8b5f8..f807497709 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -174,7 +174,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase
def test_on_rotation_can_be_passed_at_the_constructor_level
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign(encoded: "message")
- rotated = false
+ rotated = rotated = false # double assigning to suppress "assigned but unused variable" warning
encryptor = ActiveSupport::MessageEncryptor.new(@secret, on_rotation: proc { rotated = true })
encryptor.rotate secrets[:older], "older sign"
@@ -188,7 +188,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase
def test_on_rotation_option_takes_precedence_over_the_one_given_in_constructor
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign(encoded: "message")
- rotated = false
+ rotated = rotated = false # double assigning to suppress "assigned but unused variable" warning
encryptor = ActiveSupport::MessageEncryptor.new(@secret, on_rotation: proc { rotated = true })
encryptor.rotate secrets[:older], "older sign"
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index e0e0d9afc0..b0027047df 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -204,6 +204,12 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
assert_equal 3, (@chars =~ /わ/u)
end
+ def test_match_should_return_boolean_for_regexp_match
+ assert_not @chars.match?(/wrong/u)
+ assert @chars.match?(/こに/u)
+ assert @chars.match?(/ち/u)
+ end
+
def test_should_use_character_offsets_for_insert_offsets
assert_equal "", (+"").mb_chars.insert(0, "")
assert_equal "こわにちわ", @chars.insert(1, "わ")
diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb
index c9c63680e4..08277e5436 100644
--- a/activesupport/test/notifications_test.rb
+++ b/activesupport/test/notifications_test.rb
@@ -41,6 +41,27 @@ module Notifications
assert_operator event.duration, :>, 0
end
+ def test_subscribe_to_events_where_payload_is_changed_during_instrumentation
+ @notifier.subscribe do |event|
+ assert_equal "success!", event.payload[:my_key]
+ end
+
+ ActiveSupport::Notifications.instrument("foo") do |payload|
+ payload[:my_key] = "success!"
+ end
+ end
+
+ def test_subscribe_to_events_can_handle_nested_hashes_in_the_paylaod
+ @notifier.subscribe do |event|
+ assert_equal "success!", event.payload[:some_key][:key_one]
+ assert_equal "great_success!", event.payload[:some_key][:key_two]
+ end
+
+ ActiveSupport::Notifications.instrument("foo", some_key: { key_one: "success!" }) do |payload|
+ payload[:some_key][:key_two] = "great_success!"
+ end
+ end
+
def test_subscribe_via_top_level_api
old_notifier = ActiveSupport::Notifications.notifier
ActiveSupport::Notifications.notifier = ActiveSupport::Notifications::Fanout.new
diff --git a/activesupport/test/parameter_filter_test.rb b/activesupport/test/parameter_filter_test.rb
index d2dc71061d..510921cf95 100644
--- a/activesupport/test/parameter_filter_test.rb
+++ b/activesupport/test/parameter_filter_test.rb
@@ -22,16 +22,23 @@ class ParameterFilterTest < ActiveSupport::TestCase
filter_words << "blah"
filter_words << lambda { |key, value|
- value.reverse! if key =~ /bargain/
+ value.reverse! if /bargain/.match?(key)
}
filter_words << lambda { |key, value, original_params|
value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello"
}
+ filter_words << lambda { |key, value|
+ value.upcase! if key == "array_elements"
+ }
+
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } }
after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } }
+ before_filter["array_elements"] = %w(element1 element2)
+ after_filter["array_elements"] = %w(ELEMENT1 ELEMENT2)
+
assert_equal after_filter, parameter_filter.filter(before_filter)
end
end
@@ -54,7 +61,7 @@ class ParameterFilterTest < ActiveSupport::TestCase
filter_words << "blah"
filter_words << lambda { |key, value|
- value.reverse! if key =~ /bargain/
+ value.reverse! if /bargain/.match?(key)
}
filter_words << lambda { |key, value, original_params|
value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello"
diff --git a/activesupport/test/secure_compare_rotator_test.rb b/activesupport/test/secure_compare_rotator_test.rb
new file mode 100644
index 0000000000..d80faea128
--- /dev/null
+++ b/activesupport/test/secure_compare_rotator_test.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+require "active_support/secure_compare_rotator"
+
+class SecureCompareRotatorTest < ActiveSupport::TestCase
+ test "#secure_compare! works correctly after rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+
+ assert_equal(true, wrapper.secure_compare!("new_secret"))
+ end
+
+ test "#secure_compare! works correctly after multiple rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+ wrapper.rotate("another_secret")
+ wrapper.rotate("and_another_one")
+
+ assert_equal(true, wrapper.secure_compare!("and_another_one"))
+ end
+
+ test "#secure_compare! fails correctly when credential is not part of the rotation" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+
+ assert_raises(ActiveSupport::SecureCompareRotator::InvalidMatch) do
+ wrapper.secure_compare!("different_secret")
+ end
+ end
+
+ test "#secure_compare! calls the on_rotation proc" do
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret")
+ wrapper.rotate("new_secret")
+ wrapper.rotate("another_secret")
+ wrapper.rotate("and_another_one")
+
+ @witness = nil
+
+ assert_changes(:@witness, from: nil, to: true) do
+ assert_equal(true, wrapper.secure_compare!("and_another_one", on_rotation: -> { @witness = true }))
+ end
+ end
+
+ test "#secure_compare! calls the on_rotation proc that given in constructor" do
+ @witness = nil
+
+ wrapper = ActiveSupport::SecureCompareRotator.new("old_secret", on_rotation: -> { @witness = true })
+ wrapper.rotate("new_secret")
+ wrapper.rotate("another_secret")
+ wrapper.rotate("and_another_one")
+
+ assert_changes(:@witness, from: nil, to: true) do
+ assert_equal(true, wrapper.secure_compare!("and_another_one"))
+ end
+ end
+end
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index f948c363df..6ca3afd561 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -717,6 +717,13 @@ class TimeZoneTest < ActiveSupport::TestCase
assert zone !~ /Nonexistent_Place/
end
+ def test_zone_match?
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
+ assert zone.match?(/Eastern/)
+ assert zone.match?(/New_York/)
+ assert_not zone.match?(/Nonexistent_Place/)
+ end
+
def test_to_s
assert_equal "(GMT+05:30) New Delhi", ActiveSupport::TimeZone["New Delhi"].to_s
end
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index 9e29a93ea0..f13c5efa47 100644
--- a/activesupport/test/transliterate_test.rb
+++ b/activesupport/test/transliterate_test.rb
@@ -57,4 +57,53 @@ class TransliterateTest < ActiveSupport::TestCase
end
assert_equal "Can only transliterate strings. Received Object", exception.message
end
+
+ def test_transliterate_handles_strings_with_valid_utf8_encodings
+ string = String.new("A", encoding: Encoding::UTF_8).freeze
+ assert_equal "A", ActiveSupport::Inflector.transliterate(string)
+ end
+
+ def test_transliterate_handles_strings_with_valid_us_ascii_encodings
+ string = String.new("A", encoding: Encoding::US_ASCII).freeze
+ transcoded = ActiveSupport::Inflector.transliterate(string)
+ assert_equal "A", transcoded
+ assert_equal Encoding::US_ASCII, transcoded.encoding
+ end
+
+ def test_transliterate_handles_strings_with_valid_gb18030_encodings
+ string = String.new("A", encoding: Encoding::GB18030).freeze
+ transcoded = ActiveSupport::Inflector.transliterate(string)
+ assert_equal "A", transcoded
+ assert_equal Encoding::GB18030, transcoded.encoding
+ end
+
+ def test_transliterate_handles_strings_with_incompatible_encodings
+ incompatible_encodings = Encoding.list - [
+ Encoding::UTF_8,
+ Encoding::US_ASCII,
+ Encoding::GB18030
+ ]
+ incompatible_encodings.each do |encoding|
+ string = String.new("", encoding: encoding).freeze
+ exception = assert_raises ArgumentError do
+ ActiveSupport::Inflector.transliterate(string)
+ end
+ assert_equal "Can not transliterate strings with #{encoding} encoding", exception.message
+ end
+ end
+
+ def test_transliterate_handles_strings_with_invalid_utf8_bytes
+ string = String.new("\255", encoding: Encoding::UTF_8).freeze
+ assert_equal "?", ActiveSupport::Inflector.transliterate(string)
+ end
+
+ def test_transliterate_handles_strings_with_invalid_us_ascii_bytes
+ string = String.new("\255", encoding: Encoding::US_ASCII).freeze
+ assert_equal "?", ActiveSupport::Inflector.transliterate(string)
+ end
+
+ def test_transliterate_handles_strings_with_invalid_gb18030_bytes
+ string = String.new("\255", encoding: Encoding::GB18030).freeze
+ assert_equal "?", ActiveSupport::Inflector.transliterate(string)
+ end
end
diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb
index 18a3f2ca66..73e7f40b0d 100644
--- a/activesupport/test/xml_mini_test.rb
+++ b/activesupport/test/xml_mini_test.rb
@@ -5,6 +5,7 @@ require "active_support/xml_mini"
require "active_support/builder"
require "active_support/core_ext/hash"
require "active_support/core_ext/big_decimal"
+require "active_support/core_ext/date/conversions"
require "yaml"
module XmlMiniTest
diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb
index 8a0361ff4c..fc8e6b97e6 100644
--- a/guides/rails_guides/kindle.rb
+++ b/guides/rails_guides/kindle.rb
@@ -36,7 +36,7 @@ module Kindle
frontmatter = []
html_pages.delete_if { |x|
if /(toc|welcome|copyright).html/.match?(x)
- frontmatter << x unless x =~ /toc/
+ frontmatter << x unless /toc/.match?(x)
true
end
}
diff --git a/guides/source/5_2_release_notes.md b/guides/source/5_2_release_notes.md
index ac247bc3f9..7aac07dbbe 100644
--- a/guides/source/5_2_release_notes.md
+++ b/guides/source/5_2_release_notes.md
@@ -326,7 +326,7 @@ Please refer to the [Changelog][action-view] for detailed changes.
select divider `option`.
([Pull Request](https://github.com/rails/rails/pull/31088))
-* Change `form_with` to generates ids by default.
+* Change `form_with` to generate ids by default.
([Commit](https://github.com/rails/rails/commit/260d6f112a0ffdbe03e6f5051504cb441c1e94cd))
* Add `preload_link_tag` helper.
diff --git a/guides/source/6_0_release_notes.md b/guides/source/6_0_release_notes.md
index c826b19f1a..7c5478a03d 100644
--- a/guides/source/6_0_release_notes.md
+++ b/guides/source/6_0_release_notes.md
@@ -513,7 +513,7 @@ Please refer to the [Changelog][active-record] for detailed changes.
([Pull Request](https://github.com/rails/rails/pull/34742))
* Add the ability to disable scopes generated by `ActiveRecord.enum`.
- ([Pull Request](https://github.com/rails/rails/pull/34605/files))
+ ([Pull Request](https://github.com/rails/rails/pull/34605))
* Make implicit ordering configurable for a column.
([Pull Request](https://github.com/rails/rails/pull/34480))
@@ -672,6 +672,12 @@ Please refer to the [Changelog][active-storage] for detailed changes.
is saved instead of immediately.
([Pull Request](https://github.com/rails/rails/pull/33303))
+* Optionally replace existing files instead of adding to them when assigning to
+ a collection of attachments (as in `@user.update!(images: [ … ])`). Use
+ `config.active_storage.replace_on_assign_to_many` to control this behavior.
+ ([Pull Request](https://github.com/rails/rails/pull/33303),
+ [Pull Request](https://github.com/rails/rails/pull/36716))
+
* Add the ability to reflect on defined attachments using the existing
Active Record reflection mechanism.
([Pull Request](https://github.com/rails/rails/pull/33018))
@@ -688,10 +694,6 @@ Please refer to the [Changelog][active-storage] for detailed changes.
`mini_magick` directly.
([Pull Request](https://github.com/rails/rails/pull/32471))
-* Replace existing images instead of adding to them when updating an
- attached model via `update` or `update!` with, say, `@user.update!(images: [ … ])`.
- ([Pull Request](https://github.com/rails/rails/pull/33303))
-
Active Model
------------
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index f8367283fc..a5d097637e 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -34,7 +34,7 @@ Controller Naming Convention
The naming convention of controllers in Rails favors pluralization of the last word in the controller's name, although it is not strictly required (e.g. `ApplicationController`). For example, `ClientsController` is preferable to `ClientController`, `SiteAdminsController` is preferable to `SiteAdminController` or `SitesAdminsController`, and so on.
-Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and will keep URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details.
+Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and will keep named route helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details.
NOTE: The controller naming convention differs from the naming convention of models, which are expected to be named in singular form.
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index f600cf29ce..9f4a567f96 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -743,7 +743,7 @@ files (environment.rb, production.rb, etc...)
| Configuration | Description |
|---------------|-------------|
|`logger`|Generates information on the mailing run if available. Can be set to `nil` for no logging. Compatible with both Ruby's own `Logger` and `Log4r` loggers.|
-|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain` (will send the password in the clear), `:login` (will send password Base64 encoded) or `:cram_md5` (combines a Challenge/Response mechanism to exchange information and a cryptographic Message Digest 5 algorithm to hash important information)</li><li>`:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. Defaults to `true`.</li><li>`:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name of an OpenSSL verify constant ('none' or 'peer') or directly the constant (`OpenSSL::SSL::VERIFY_NONE` or `OpenSSL::SSL::VERIFY_PEER`).</li></ul>|
+|`smtp_settings`|Allows detailed configuration for `:smtp` delivery method:<ul><li>`:address` - Allows you to use a remote mail server. Just change it from its default `"localhost"` setting.</li><li>`:port` - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li>`:domain` - If you need to specify a HELO domain, you can do it here.</li><li>`:user_name` - If your mail server requires authentication, set the username in this setting.</li><li>`:password` - If your mail server requires authentication, set the password in this setting.</li><li>`:authentication` - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of `:plain` (will send the password in the clear), `:login` (will send password Base64 encoded) or `:cram_md5` (combines a Challenge/Response mechanism to exchange information and a cryptographic Message Digest 5 algorithm to hash important information)</li><li>`:enable_starttls_auto` - Detects if STARTTLS is enabled in your SMTP server and starts to use it. Defaults to `true`.</li><li>`:openssl_verify_mode` - When using TLS, you can set how OpenSSL checks the certificate. This is really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name of an OpenSSL verify constant ('none' or 'peer') or directly the constant (`OpenSSL::SSL::VERIFY_NONE` or `OpenSSL::SSL::VERIFY_PEER`).</li><li>`:ssl/:tls` - Enables the SMTP connection to use SMTP/TLS (SMTPS: SMTP over direct TLS connection)</li></ul>|
|`sendmail_settings`|Allows you to override options for the `:sendmail` delivery method.<ul><li>`:location` - The location of the sendmail executable. Defaults to `/usr/sbin/sendmail`.</li><li>`:arguments` - The command line arguments to be passed to sendmail. Defaults to `-i`.</li></ul>|
|`raise_delivery_errors`|Whether or not errors should be raised if the email fails to be delivered. This only works if the external email server is configured for immediate delivery.|
|`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](https://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.|
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index a1b69edd22..dda3ae0863 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -1025,6 +1025,34 @@ If `@article.author_id` is 1, this would return:
<label for="article_author_id_3">M. Clark</label>
```
+Recovering some option passed (e.g. programatically checking an object from collection):
+
+```ruby
+collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial, {checked: Author.last})
+```
+
+In this case, the last object from the collection will be checked:
+
+```html
+<input id="article_author_id_1" name="article[author_id]" type="radio" value="1" />
+<label for="article_author_id_1">D. Heinemeier Hansson</label>
+<input id="article_author_id_2" name="article[author_id]" type="radio" value="2" />
+<label for="article_author_id_2">D. Thomas</label>
+<input id="article_author_id_3" name="article[author_id]" type="radio" value="3" checked="checked" />
+<label for="article_author_id_3">M. Clark</label>
+```
+
+To access the passed options programatically (e.g. adding a custom class if checked):
+
+**Sample html.erb**
+
+```html+erb
+<%= collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial, {checked: Author.last, required: true} do |rb| %>
+ <%= rb.label(class: "#{'my-custom-class' if rb.value == Author.last.id}") { rb.radio_button + rb.text } %>
+<% end %>
+```
+
+
#### collection_check_boxes
Returns `check_box` tags for the collection of existing return values of `method` for `object`'s class.
diff --git a/guides/source/active_record_multiple_databases.md b/guides/source/active_record_multiple_databases.md
index d7d9bd1ca9..07be21a254 100644
--- a/guides/source/active_record_multiple_databases.md
+++ b/guides/source/active_record_multiple_databases.md
@@ -180,7 +180,7 @@ to primary based on the HTTP verb and whether there was a recent write.
If the application is receiving a POST, PUT, DELETE, or PATCH request the application will
automatically write to the primary. For the specified time after the write the application
-will read from the replica. For a GET or HEAD request the application will read from the
+will read from the primary. For a GET or HEAD request the application will read from the
replica unless there was a recent write.
To activate the automatic connection switching middleware, add or uncomment the following
diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md
index 932a5dc2e9..54f8f5c2b5 100644
--- a/guides/source/active_storage_overview.md
+++ b/guides/source/active_storage_overview.md
@@ -43,6 +43,8 @@ tables. Use `rails db:migrate` to run the migration.
WARNING: `active_storage_attachments` is a polymorphic join table that stores your model's class name. If your model's class name changes, you will need to run a migration on this table to update the underlying `record_type` to your model's new class name.
+WARNING: If you are using UUIDs instead of integers as the primary key on your models you will need to change the column type of `record_id` for the `active_storage_attachments` table in the generated migration accordingly.
+
Declare Active Storage services in `config/storage.yml`. For each service your
application uses, provide a name and the requisite configuration. The example
below declares three services named `local`, `test`, and `amazon`:
@@ -398,6 +400,10 @@ helper allows you to set the disposition.
rails_blob_path(user.avatar, disposition: "attachment")
```
+WARNING: To prevent XSS attacks, ActiveStorage forces the Content-Disposition header
+to "attachment" for some kind of files. To change this behaviour see the
+available configuration opions in [Configuring Rails Applications](configuring.html#configuring-active-storage).
+
If you need to create a link from outside of controller/view context (Background
jobs, Cronjobs, etc.), you can access the rails_blob_path like this:
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 8cb49ca6ae..f36cacfe8d 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -155,15 +155,6 @@ Complex(1).duplicable? # => true
1.method(:+).duplicable? # => false
```
-`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
-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.
NOTE: Defined in `active_support/core_ext/object/duplicable.rb`.
@@ -2358,10 +2349,6 @@ There's also a related idiom that uses the splat operator:
[*object]
```
-which in Ruby 1.8 returns `[nil]` for `nil`, and calls to `Array(object)` otherwise. (Please if you know the exact behavior in 1.9 contact fxn.)
-
-Thus, in this case the behavior is different for `nil`, and the differences with `Kernel#Array` explained above apply to the rest of `object`s.
-
NOTE: Defined in `active_support/core_ext/array/wrap.rb`.
### Duplicating
diff --git a/guides/source/autoloading_and_reloading_constants.md b/guides/source/autoloading_and_reloading_constants.md
index 7dfc39e192..2556119d33 100644
--- a/guides/source/autoloading_and_reloading_constants.md
+++ b/guides/source/autoloading_and_reloading_constants.md
@@ -1,18 +1,18 @@
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
-Autoloading and Reloading Constants
-===================================
+Autoloading and Reloading Constants (Zeitwerk Mode)
+======================================================
-This guide documents how constant autoloading and reloading works.
+This guide documents how autoloading and reloading works in `zeitwerk` mode.
After reading this guide, you will know:
-* Key aspects of Ruby constants
-* What are the `autoload_paths` and how does eager loading work in production?
-* How constant autoloading works
-* What is `require_dependency`
-* How constant reloading works
-* Solutions to common autoloading gotchas
+* Autoloading modes
+* Related Rails configuration
+* Project structure
+* Autoloading, reloading, and eager loading
+* Single Table Inheritance
+* And more
--------------------------------------------------------------------------------
@@ -20,13 +20,15 @@ After reading this guide, you will know:
Introduction
------------
-Ruby on Rails allows applications to be written as if their code was preloaded.
+INFO. This guide documents autoloading in `zeitwerk` mode, which is new in Rails 6. If you'd like to read about `classic` mode instead, please check [Autoloading and Reloading Constants (Classic Mode)](autoloading_and_reloading_constants_classic_mode.html).
-In a normal Ruby program classes need to load their dependencies:
+In a normal Ruby program, dependencies need to be loaded by hand. For example, the following controller uses classes `ApplicationController` and `Post`, and normally you'd need to put `require` calls for them:
```ruby
-require 'application_controller'
-require 'post'
+# DO NOT DO THIS.
+require "application_controller"
+require "post"
+# DO NOT DO THIS.
class PostsController < ApplicationController
def index
@@ -35,16 +37,7 @@ class PostsController < ApplicationController
end
```
-Our Rubyist instinct quickly sees some redundancy in there: If classes were
-defined in files matching their name, couldn't their loading be automated
-somehow? We could save scanning the file for dependencies, which is brittle.
-
-Moreover, `Kernel#require` loads files once, but development is much more smooth
-if code gets refreshed when it changes without restarting the server. It would
-be nice to be able to use `Kernel#load` in development, and `Kernel#require` in
-production.
-
-Indeed, those features are provided by Ruby on Rails, where we just write
+This is not the case in Rails applications, where application classes and modules are just available everywhere:
```ruby
class PostsController < ApplicationController
@@ -54,1330 +47,255 @@ class PostsController < ApplicationController
end
```
-This guide documents how that works.
-
-
-Constants Refresher
--------------------
-
-While constants are trivial in most programming languages, they are a rich
-topic in Ruby.
-
-It is beyond the scope of this guide to document Ruby constants, but we are
-nevertheless going to highlight a few key topics. Truly grasping the following
-sections is instrumental to understanding constant autoloading and reloading.
-
-### Nesting
-
-Class and module definitions can be nested to create namespaces:
-
-```ruby
-module XML
- class SAXParser
- # (1)
- end
-end
-```
-
-The *nesting* at any given place is the collection of enclosing nested class and
-module objects outwards. The nesting at any given place can be inspected with
-`Module.nesting`. For example, in the previous example, the nesting at
-(1) is
-
-```ruby
-[XML::SAXParser, XML]
-```
-
-It is important to understand that the nesting is composed of class and module
-*objects*, it has nothing to do with the constants used to access them, and is
-also unrelated to their names.
-
-For instance, while this definition is similar to the previous one:
-
-```ruby
-class XML::SAXParser
- # (2)
-end
-```
-
-the nesting in (2) is different:
-
-```ruby
-[XML::SAXParser]
-```
-
-`XML` does not belong to it.
-
-We can see in this example that the name of a class or module that belongs to a
-certain nesting does not necessarily correlate with the namespaces at the spot.
-
-Even more, they are totally independent, take for instance
-
-```ruby
-module X
- module Y
- end
-end
-
-module A
- module B
- end
-end
-
-module X::Y
- module A::B
- # (3)
- end
-end
-```
-
-The nesting in (3) consists of two module objects:
-
-```ruby
-[A::B, X::Y]
-```
-
-So, it not only doesn't end in `A`, which does not even belong to the nesting,
-but it also contains `X::Y`, which is independent from `A::B`.
-
-The nesting is an internal stack maintained by the interpreter, and it gets
-modified according to these rules:
-
-* The class object following a `class` keyword gets pushed when its body is
-executed, and popped after it.
-
-* The module object following a `module` keyword gets pushed when its body is
-executed, and popped after it.
-
-* A singleton class opened with `class << object` gets pushed, and popped later.
-
-* When `instance_eval` is called using a string argument,
-the singleton class of the receiver is pushed to the nesting of the eval'ed
-code. When `class_eval` or `module_eval` is called using a string argument,
-the receiver is pushed to the nesting of the eval'ed code.
-
-* The nesting at the top-level of code interpreted by `Kernel#load` is empty
-unless the `load` call receives a true value as second argument, in which case
-a newly created anonymous module is pushed by Ruby.
-
-It is interesting to observe that blocks do not modify the stack. In particular
-the blocks that may be passed to `Class.new` and `Module.new` do not get the
-class or module being defined pushed to their nesting. That's one of the
-differences between defining classes and modules in one way or another.
-
-### Class and Module Definitions are Constant Assignments
-
-Let's suppose the following snippet creates a class (rather than reopening it):
-
-```ruby
-class C
-end
-```
-
-Ruby creates a constant `C` in `Object` and stores in that constant a class
-object. The name of the class instance is "C", a string, named after the
-constant.
+Idiomatic Rails applications only issue `require` calls to load stuff from their `lib` directory, the Ruby standard library, Ruby gems, etc. That is, anything that does not belong to their autoload paths, explained below.
-That is,
-```ruby
-class Project < ApplicationRecord
-end
-```
+Enabling Zeitwerk Mode
+----------------------
-performs a constant assignment equivalent to
+The autoloading `zeitwerk` mode is enabled by default in Rails 6 applications running on CRuby:
```ruby
-Project = Class.new(ApplicationRecord)
+# config/application.rb
+config.load_defaults "6.x" # enables zeitwerk mode in CRuby
```
-including setting the name of the class as a side-effect:
+In `zeitwerk` mode, Rails uses [Zeitwerk](https://github.com/fxn/zeitwerk) internally to autoload, reload, and eager load. Rails instantiates and configures a dedicated Zeitwerk instance that manages the project.
-```ruby
-Project.name # => "Project"
-```
+INFO. You do not configure Zeitwerk manually in a Rails application. Rather, you configure the application using the portable configuration points explained in this guide, and Rails translates that to Zeitwerk on your behalf.
-Constant assignment has a special rule to make that happen: if the object
-being assigned is an anonymous class or module, Ruby sets the object's name to
-the name of the constant.
+Project Structure
+-----------------
-INFO. From then on, what happens to the constant and the instance does not
-matter. For example, the constant could be deleted, the class object could be
-assigned to a different constant, be stored in no constant anymore, etc. Once
-the name is set, it doesn't change.
+In a Rails application file names have to match the constants they define, with directories acting as namespaces.
-Similarly, module creation using the `module` keyword as in
+For example, the file `app/helpers/users_helper.rb` should define `UsersHelper` and the file `app/controllers/admin/payments_controller.rb` should define `Admin::PaymentsController`.
-```ruby
-module Admin
-end
-```
-
-performs a constant assignment equivalent to
+Rails configures Zeitwerk to inflect file names with `String#camelize`. For example, it expects that `app/controllers/users_controller.rb` defines the constant `UsersController` because
```ruby
-Admin = Module.new
+"users_controller".camelize # => UsersController
```
-including setting the name as a side-effect:
+If you need to customize any of these inflections, for example to add an acronym, please have a look at `config/initializers/inflections.rb`.
-```ruby
-Admin.name # => "Admin"
-```
+Please, check the [Zeitwerk documentation](https://github.com/fxn/zeitwerk#file-structure) for further details.
-WARNING. The execution context of a block passed to `Class.new` or `Module.new`
-is not entirely equivalent to the one of the body of the definitions using the
-`class` and `module` keywords. But both idioms result in the same constant
-assignment.
-
-Thus, an informal expression like "the `String` class" technically means the
-class object stored in the constant called "String". That constant, in turn,
-belongs to the class object stored in the constant called "Object".
-
-`String` is an ordinary constant, and everything related to them such as
-resolution algorithms applies to it.
-
-Likewise, in the controller
-
-```ruby
-class PostsController < ApplicationController
- def index
- @posts = Post.all
- end
-end
-```
-
-`Post` is not syntax for a class. Rather, `Post` is a regular Ruby constant. If
-all is good, the constant is evaluated to an object that responds to `all`.
+Autoload paths
+--------------
-That is why we talk about *constant* autoloading, Rails has the ability to
-load constants on the fly.
+We call _autoload paths_ to the list of application directories whose contents are to be autoloaded. For example, `app/models`. Such directories represent the root namespace: `Object`.
-### Constants are Stored in Modules
+INFO. Autoload paths are called _root directories_ in Zeitwerk documentation, but we'll stay with "autoload path" in this guide.
-Constants belong to modules in a very literal sense. Classes and modules have
-a constant table; think of it as a hash table.
+Within an autoload path, file names must match the constants they define as documented [here](https://github.com/fxn/zeitwerk#file-structure).
-Let's analyze an example to really understand what that means. While common
-abuses of language like "the `String` class" are convenient, the exposition is
-going to be precise here for didactic purposes.
+By default, the autoload paths of an application consist of all the subdirectories of `app` that exist when the application boots ---except for `assets`, `javascripts`, `views`,--- plus the autoload paths of engines it might depend on.
-Let's consider the following module definition:
+For example, if `UsersHelper` is implemented in `app/helpers/users_helper.rb`, the module is autoloadable, you do not need (and should not write) a `require` call for it:
-```ruby
-module Colors
- RED = '0xff0000'
-end
```
-
-First, when the `module` keyword is processed, the interpreter creates a new
-entry in the constant table of the class object stored in the `Object` constant.
-Said entry associates the name "Colors" to a newly created module object.
-Furthermore, the interpreter sets the name of the new module object to be the
-string "Colors".
-
-Later, when the body of the module definition is interpreted, a new entry is
-created in the constant table of the module object stored in the `Colors`
-constant. That entry maps the name "RED" to the string "0xff0000".
-
-In particular, `Colors::RED` is totally unrelated to any other `RED` constant
-that may live in any other class or module object. If there were any, they
-would have separate entries in their respective constant tables.
-
-Pay special attention in the previous paragraphs to the distinction between
-class and module objects, constant names, and value objects associated to them
-in constant tables.
-
-### Resolution Algorithms
-
-#### Resolution Algorithm for Relative Constants
-
-At any given place in the code, let's define *cref* to be the first element of
-the nesting if it is not empty, or `Object` otherwise.
-
-Without getting too much into the details, the resolution algorithm for relative
-constant references goes like this:
-
-1. If the nesting is not empty the constant is looked up in its elements and in
-order. The ancestors of those elements are ignored.
-
-2. If not found, then the algorithm walks up the ancestor chain of the cref.
-
-3. If not found and the cref is a module, the constant is looked up in `Object`.
-
-4. If not found, `const_missing` is invoked on the cref. The default
-implementation of `const_missing` raises `NameError`, but it can be overridden.
-
-Rails autoloading **does not emulate this algorithm**, but its starting point is
-the name of the constant to be autoloaded, and the cref. See more in [Relative
-References](#autoloading-algorithms-relative-references).
-
-#### Resolution Algorithm for Qualified Constants
-
-Qualified constants look like this:
-
-```ruby
-Billing::Invoice
+$ bin/rails runner 'p UsersHelper'
+UsersHelper
```
-`Billing::Invoice` is composed of two constants: `Billing` is relative and is
-resolved using the algorithm of the previous section.
-
-INFO. Leading colons would make the first segment absolute rather than
-relative: `::Billing::Invoice`. That would force `Billing` to be looked up
-only as a top-level constant.
-
-`Invoice` on the other hand is qualified by `Billing` and we are going to see
-its resolution next. Let's define *parent* to be that qualifying class or module
-object, that is, `Billing` in the example above. The algorithm for qualified
-constants goes like this:
-
-1. The constant is looked up in the parent and its ancestors. In Ruby >= 2.5,
-`Object` is skipped if present among the ancestors. `Kernel` and `BasicObject`
-are still checked though.
+Autoload paths automatically pick any custom directories under `app`. For example, if your application has `app/presenters`, or `app/services`, etc., they are added to autoload paths.
-2. If the lookup fails, `const_missing` is invoked in the parent. The default
-implementation of `const_missing` raises `NameError`, but it can be overridden.
+The array of autoload paths can be extended by mutating `config.autoload_paths`, in `config/application.rb`, but nowadays this is discouraged.
-INFO. In Ruby < 2.5 `String::Hash` evaluates to `Hash` and the interpreter
-issues a warning: "toplevel constant Hash referenced by String::Hash". Starting
-with 2.5, `String::Hash` raises `NameError` because `Object` is skipped.
+WARNING. Please, do not mutate `ActiveSupport::Dependencies.autoload_paths`, the public interface to change autoload paths is `config.autoload_paths`.
-As you see, this algorithm is simpler than the one for relative constants. In
-particular, the nesting plays no role here, and modules are not special-cased,
-if neither they nor their ancestors have the constants, `Object` is **not**
-checked.
-Rails autoloading **does not emulate this algorithm**, but its starting point is
-the name of the constant to be autoloaded, and the parent. See more in
-[Qualified References](#autoloading-algorithms-qualified-references).
-
-
-Vocabulary
+$LOAD_PATH
----------
-### Parent Namespaces
-
-Given a string with a constant path we define its *parent namespace* to be the
-string that results from removing its rightmost segment.
-
-For example, the parent namespace of the string "A::B::C" is the string "A::B",
-the parent namespace of "A::B" is "A", and the parent namespace of "A" is "".
-
-The interpretation of a parent namespace when thinking about classes and modules
-is tricky though. Let's consider a module M named "A::B":
-
-* The parent namespace, "A", may not reflect nesting at a given spot.
-
-* The constant `A` may no longer exist, some code could have removed it from
-`Object`.
-
-* If `A` exists, the class or module that was originally in `A` may not be there
-anymore. For example, if after a constant removal there was another constant
-assignment there would generally be a different object in there.
-
-* In such case, it could even happen that the reassigned `A` held a new class or
-module called also "A"!
-
-* In the previous scenarios M would no longer be reachable through `A::B` but
-the module object itself could still be alive somewhere and its name would
-still be "A::B".
-
-The idea of a parent namespace is at the core of the autoloading algorithms
-and helps explain and understand their motivation intuitively, but as you see
-that metaphor leaks easily. Given an edge case to reason about, take always into
-account that by "parent namespace" the guide means exactly that specific string
-derivation.
-
-### Loading Mechanism
-
-Rails autoloads files with `Kernel#load` when `config.cache_classes` is false,
-the default in development mode, and with `Kernel#require` otherwise, the
-default in production mode.
-
-`Kernel#load` allows Rails to execute files more than once if [constant
-reloading](#constant-reloading) is enabled.
-
-This guide uses the word "load" freely to mean a given file is interpreted, but
-the actual mechanism can be `Kernel#load` or `Kernel#require` depending on that
-flag.
-
-
-Autoloading Availability
-------------------------
-
-Rails is always able to autoload provided its environment is in place. For
-example the `runner` command autoloads:
-
-```
-$ rails runner 'p User.column_names'
-["id", "email", "created_at", "updated_at"]
-```
-
-The console autoloads, the test suite autoloads, and of course the application
-autoloads.
-
-By default, Rails eager loads the application files when it boots in production
-mode, so most of the autoloading going on in development does not happen. But
-autoloading may still be triggered during eager loading.
-
-For example, given
-
-```ruby
-class BeachHouse < House
-end
-```
-
-if `House` is still unknown when `app/models/beach_house.rb` is being eager
-loaded, Rails autoloads it.
-
-
-autoload_paths and eager_load_paths
------------------------------------
-
-As you probably know, when `require` gets a relative file name:
+Autoload paths are added to `$LOAD_PATH` by default. However, Zeitwerk uses absolute file names internally, and your application should not issue `require` calls for autoloadable files, so those directories are actually not needed there. You can opt-out with this flag:
```ruby
-require 'erb'
+config.add_autoload_paths_to_load_path = false
```
-Ruby looks for the file in the directories listed in `$LOAD_PATH`. That is, Ruby
-iterates over all its directories and for each one of them checks whether they
-have a file called "erb.rb", or "erb.so", or "erb.o", or "erb.dll". If it finds
-any of them, the interpreter loads it and ends the search. Otherwise, it tries
-again in the next directory of the list. If the list gets exhausted, `LoadError`
-is raised.
-
-We are going to cover how constant autoloading works in more detail later, but
-the idea is that when a constant like `Post` is hit and missing, if there's a
-`post.rb` file for example in `app/models` Rails is going to find it, evaluate
-it, and have `Post` defined as a side-effect.
-
-All right, Rails has a collection of directories similar to `$LOAD_PATH` in which
-to look up `post.rb`. That collection is called `autoload_paths` and by
-default it contains:
-
-* All subdirectories of `app` in the application and engines present at boot
- time. For example, `app/controllers`. They do not need to be the default
- ones, any custom directories like `app/workers` belong automatically to
- `autoload_paths`.
+That may speed legit `require` calls a bit, since there are less lookups. Also, if your application uses [Bootsnap](https://github.com/Shopify/bootsnap), that saves the library from building unnecessary indexes, and saves the RAM they would need.
-* Any existing second level directories called `app/*/concerns` in the
- application and engines.
-* The directory `test/mailers/previews`.
+Reloading
+---------
-`eager_load_paths` is initially the `app` paths above
+Rails automatically reloads classes and modules if application files change.
-How files are autoloaded depends on `eager_load` and `cache_classes` config settings which typically vary in development, production, and test modes:
+More precisely, if the web server is running and application files have been modified, Rails unloads all autoloaded constants just before the next request is processed. That way, application classes or modules used during that request are going to be autoloaded, thus picking up their current implementation in the file system.
- * In **development**, you want quicker startup with incremental loading of application code. So `eager_load` should be set to `false`, and Rails will autoload files as needed (see [Autoloading Algorithms](#autoloading-algorithms) below) -- and then reload them when they change (see [Constant Reloading](#constant-reloading) below).
- * In **production**, however, you want consistency and thread-safety and can live with a longer boot time. So `eager_load` is set to `true`, and then during boot (before the app is ready to receive requests) Rails loads all files in the `eager_load_paths` and then turns off auto loading (NB: autoloading may be needed during eager loading). Not autoloading after boot is a `good thing`, as autoloading can cause the app to be have thread-safety problems.
- * In **test**, for speed of execution (of individual tests) `eager_load` is `false`, so Rails follows development behaviour.
+Reloading can be enabled or disabled. The setting that controls this behavior is `config.cache_classes`, which is false by default in `development` mode (reloading enabled), and true by default in `production` mode (reloading disabled).
-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.
+Rails detects files have changed using an evented file monitor (default), or walking the autoload paths, depending on `config.file_watcher`.
-See also [Autoloading in the Test Environment](#autoloading-in-the-test-environment).
+In a Rails console there is no file watcher active regardless of the value of `config.cache_classes`. This is so because, normally, it would be confusing to have code reloaded in the middle of a console session, the same way you generally want an individual request to be served by a consistent, non-changing set of application classes and modules.
-`config.autoload_paths` is not changeable from environment-specific configuration files.
-
-The value of `autoload_paths` can be inspected. In a just-generated application
-it is (edited):
+However, you can force a reload in the console executing `reload!`:
```
-$ rails r 'puts ActiveSupport::Dependencies.autoload_paths'
-.../app/assets
-.../app/channels
-.../app/controllers
-.../app/controllers/concerns
-.../app/helpers
-.../app/jobs
-.../app/mailers
-.../app/models
-.../app/models/concerns
-.../activestorage/app/assets
-.../activestorage/app/controllers
-.../activestorage/app/javascript
-.../activestorage/app/jobs
-.../activestorage/app/models
-.../actioncable/app/assets
-.../actionview/app/assets
-.../test/mailers/previews
+$ bin/rails c
+Loading development environment (Rails 6.0.0)
+irb(main):001:0> User.object_id
+=> 70136277390120
+irb(main):002:0> reload!
+Reloading...
+=> true
+irb(main):003:0> User.object_id
+=> 70136284426020
```
-INFO. `autoload_paths` is computed and cached during the initialization process.
-The application needs to be restarted to reflect any changes in the directory
-structure.
+as you can see, the class object stored in the `User` constant is different after reloading.
+### Reloading and Stale Objects
-Autoloading Algorithms
-----------------------
+It is very important to understand that Ruby does not have a way to truly reload classes and modules in memory, and have that reflected everywhere they are already used. Technically, "unloading" the `User` class means removing the `User` constant via `Object.send(:remove_const, "User")`.
-### Relative References
+Therefore, if you store a reloadable class or module object in a place that is not reloaded, that value is going to become stale.
-A relative constant reference may appear in several places, for example, in
+For example, if an initializer stores and caches a certain class object
```ruby
-class PostsController < ApplicationController
- def index
- @posts = Post.all
- end
-end
+# config/initializers/configure_payment_gateway.rb
+# DO NOT DO THIS.
+$PAYMENT_GATEWAY = Rails.env.production? ? RealGateway : MockedGateway
+# DO NOT DO THIS.
```
-all three constant references are relative.
-
-#### Constants after the `class` and `module` Keywords
-
-Ruby performs a lookup for the constant that follows a `class` or `module`
-keyword because it needs to know if the class or module is going to be created
-or reopened.
-
-If the constant is not defined at that point it is not considered to be a
-missing constant, autoloading is **not** triggered.
-
-So, in the previous example, if `PostsController` is not defined when the file
-is interpreted Rails autoloading is not going to be triggered, Ruby will just
-define the controller.
-
-#### Top-Level Constants
-
-On the contrary, if `ApplicationController` is unknown, the constant is
-considered missing and an autoload is going to be attempted by Rails.
-
-In order to load `ApplicationController`, Rails iterates over `autoload_paths`.
-First it checks if `app/assets/application_controller.rb` exists. If it does not,
-which is normally the case, it continues and finds
-`app/controllers/application_controller.rb`.
+and `MockedGateway` gets reloaded, `$PAYMENT_GATEWAY` still stores the class object `MockedGateway` evaluated to when the initializer ran. Reloading does not change the class object stored in `$PAYMENT_GATEWAY`.
-If the file defines the constant `ApplicationController` all is fine, otherwise
-`LoadError` is raised:
+Similarly, in the Rails console, if you have a user instance and reload:
```
-unable to autoload constant ApplicationController, expected
-<full path to application_controller.rb> to define it (LoadError)
+> user = User.new
+> reload!
```
-INFO. Rails does not require the value of autoloaded constants to be a class or
-module object. For example, if the file `app/models/max_clients.rb` defines
-`MAX_CLIENTS = 100` autoloading `MAX_CLIENTS` works just fine.
-
-#### Namespaces
+the `user` object is instance of a stale class object. Ruby gives you a new class if you evaluate `User` again, but does not update the class `user` is instance of.
-Autoloading `ApplicationController` looks directly under the directories of
-`autoload_paths` because the nesting in that spot is empty. The situation of
-`Post` is different, the nesting in that line is `[PostsController]` and support
-for namespaces comes into play.
-
-The basic idea is that given
+Another use case of this gotcha is subclassing reloadable classes in a place that is not reloaded:
```ruby
-module Admin
- class BaseController < ApplicationController
- @@all_roles = Role.all
- end
+# lib/vip_user.rb
+class VipUser < User
end
```
-to autoload `Role` we are going to check if it is defined in the current or
-parent namespaces, one at a time. So, conceptually we want to try to autoload
-any of
-
-```
-Admin::BaseController::Role
-Admin::Role
-Role
-```
-
-in that order. That's the idea. To do so, Rails looks in `autoload_paths`
-respectively for file names like these:
-
-```
-admin/base_controller/role.rb
-admin/role.rb
-role.rb
-```
-
-modulus some additional directory lookups we are going to cover soon.
-
-INFO. `'Constant::Name'.underscore` gives the relative path without extension of
-the file name where `Constant::Name` is expected to be defined.
-
-Let's see how Rails autoloads the `Post` constant in the `PostsController`
-above assuming the application has a `Post` model defined in
-`app/models/post.rb`.
-
-First it checks for `posts_controller/post.rb` in `autoload_paths`:
-
-```
-app/assets/posts_controller/post.rb
-app/controllers/posts_controller/post.rb
-app/helpers/posts_controller/post.rb
-...
-test/mailers/previews/posts_controller/post.rb
-```
-
-Since the lookup is exhausted without success, a similar search for a directory
-is performed, we are going to see why in the [next section](#automatic-modules):
+if `User` is reloaded, since `VipUser` is not, the superclass of `VipUser` is the original stale class object.
-```
-app/assets/posts_controller/post
-app/controllers/posts_controller/post
-app/helpers/posts_controller/post
-...
-test/mailers/previews/posts_controller/post
-```
+Bottom line: **do not cache reloadable classes or modules**.
-If all those attempts fail, then Rails starts the lookup again in the parent
-namespace. In this case only the top-level remains:
-```
-app/assets/post.rb
-app/controllers/post.rb
-app/helpers/post.rb
-app/mailers/post.rb
-app/models/post.rb
-```
+Eager Loading
+-------------
-A matching file is found in `app/models/post.rb`. The lookup stops there and the
-file is loaded. If the file actually defines `Post` all is fine, otherwise
-`LoadError` is raised.
+In production-like environments it is generally better to load all the application code when the application boots. Eager loading puts everything in memory ready to serve requests right away, and it is also [CoW](https://en.wikipedia.org/wiki/Copy-on-write)-friendly.
-### Qualified References
+Eager loading is controlled by the flag `config.eager_load`, which is enabled by default in `production` mode.
-When a qualified constant is missing Rails does not look for it in the parent
-namespaces. But there is a caveat: when a constant is missing, Rails is
-unable to tell if the trigger was a relative reference or a qualified one.
+The order in which files are eager loaded is undefined.
-For example, consider
+if the `Zeitwerk` constant is defined, Rails invokes `Zeitwerk::Loader.eager_load_all` regardless of the application autoloading mode. That ensures dependencies managed by Zeitwerk are eager loaded.
-```ruby
-module Admin
- User
-end
-```
-and
-
-```ruby
-Admin::User
-```
-
-If `User` is missing, in either case all Rails knows is that a constant called
-"User" was missing in a module called "Admin".
+Single Table Inheritance
+------------------------
-If there is a top-level `User` Ruby would resolve it in the former example, but
-wouldn't in the latter. In general, Rails does not emulate the Ruby constant
-resolution algorithms, but in this case it tries using the following heuristic:
+Single Table Inheritance is a feature that doesn't play well with lazy loading. Reason is, its API generally needs to be able to enumerate the STI hierarchy to work correctly, whereas lazy loading defers loading classes until they are referenced. You can't enumerate what you haven't referenced yet.
-> If none of the parent namespaces of the class or module has the missing
-> constant then Rails assumes the reference is relative. Otherwise qualified.
+In a sense, applications need to eager load STI hierarchies regardless of the loading mode.
-For example, if this code triggers autoloading
+Of course, if the application eager loads on boot, that is already accomplished. When it does not, it is in practice enough to instantiate the existing types in the database, which in development or test modes is usually fine. One way to do that is to throw this module into the `lib` directory:
```ruby
-Admin::User
-```
-
-and the `User` constant is already present in `Object`, it is not possible that
-the situation is
-
-```ruby
-module Admin
- User
-end
-```
-
-because otherwise Ruby would have resolved `User` and no autoloading would have
-been triggered in the first place. Thus, Rails assumes a qualified reference and
-considers the file `admin/user.rb` and directory `admin/user` to be the only
-valid options.
-
-In practice, this works quite well as long as the nesting matches all parent
-namespaces respectively and the constants that make the rule apply are known at
-that time.
-
-However, autoloading happens on demand. If by chance the top-level `User` was
-not yet loaded, then Rails assumes a relative reference by contract.
-
-Naming conflicts of this kind are rare in practice, but if one occurs,
-`require_dependency` provides a solution by ensuring that the constant needed
-to trigger the heuristic is defined in the conflicting place.
-
-### Automatic Modules
-
-When a module acts as a namespace, Rails does not require the application to
-define a file for it, a directory matching the namespace is enough.
+module StiPreload
+ unless Rails.application.config.eager_load
+ extend ActiveSupport::Concern
-Suppose an application has a back office whose controllers are stored in
-`app/controllers/admin`. If the `Admin` module is not yet loaded when
-`Admin::UsersController` is hit, Rails needs first to autoload the constant
-`Admin`.
-
-If `autoload_paths` has a file called `admin.rb` Rails is going to load that
-one, but if there's no such file and a directory called `admin` is found, Rails
-creates an empty module and assigns it to the `Admin` constant on the fly.
-
-### Generic Procedure
-
-Relative references are reported to be missing in the cref where they were hit,
-and qualified references are reported to be missing in their parent (see
-[Resolution Algorithm for Relative
-Constants](#resolution-algorithm-for-relative-constants) at the beginning of
-this guide for the definition of *cref*, and [Resolution Algorithm for Qualified
-Constants](#resolution-algorithm-for-qualified-constants) for the definition of
-*parent*).
-
-The procedure to autoload constant `C` in an arbitrary situation is as follows:
-
-```
-if the class or module in which C is missing is Object
- let ns = ''
-else
- let M = the class or module in which C is missing
-
- if M is anonymous
- let ns = ''
- else
- let ns = M.name
- end
-end
-
-loop do
- # Look for a regular file.
- for dir in autoload_paths
- if the file "#{dir}/#{ns.underscore}/c.rb" exists
- load/require "#{dir}/#{ns.underscore}/c.rb"
-
- if C is now defined
- return
- else
- raise LoadError
- end
+ included do
+ cattr_accessor :preloaded, instance_accessor: false
end
- end
- # Look for an automatic module.
- for dir in autoload_paths
- if the directory "#{dir}/#{ns.underscore}/c" exists
- if ns is an empty string
- let C = Module.new in Object and return
- else
- let C = Module.new in ns.constantize and return
+ class_methods do
+ def descendants
+ preload_sti unless preloaded
+ super
end
- end
- end
-
- if ns is empty
- # We reached the top-level without finding the constant.
- raise NameError
- else
- if C exists in any of the parent namespaces
- # Qualified constants heuristic.
- raise NameError
- else
- # Try again in the parent namespace.
- let ns = the parent namespace of ns and retry
- end
- end
-end
-```
-
-
-require_dependency
-------------------
-
-Constant autoloading is triggered on demand and therefore code that uses a
-certain constant may have it already defined or may trigger an autoload. That
-depends on the execution path and it may vary between runs.
-
-There are times, however, in which you want to make sure a certain constant is
-known when the execution reaches some code. `require_dependency` provides a way
-to load a file using the current [loading mechanism](#loading-mechanism), and
-keeping track of constants defined in that file as if they were autoloaded to
-have them reloaded as needed.
-
-`require_dependency` is rarely needed, but see a couple of use cases in
-[Autoloading and STI](#autoloading-and-sti) and [When Constants aren't
-Triggered](#when-constants-aren-t-missed).
-
-WARNING. Unlike autoloading, `require_dependency` does not expect the file to
-define any particular constant. Exploiting this behavior would be a bad practice
-though, file and constant paths should match.
-
-
-Constant Reloading
-------------------
-
-When `config.cache_classes` is false Rails is able to reload autoloaded
-constants.
-For example, if you're in a console session and edit some file behind the
-scenes, the code can be reloaded with the `reload!` command:
-
-```
-> reload!
-```
-
-When the application runs, code is reloaded when something relevant to this
-logic changes. In order to do that, Rails monitors a number of things:
-
-* `config/routes.rb`.
-
-* Locales.
-
-* Ruby files under `autoload_paths`.
-
-* `db/schema.rb` and `db/structure.sql`.
-
-If anything in there changes, there is a middleware that detects it and reloads
-the code.
-
-Autoloading keeps track of autoloaded constants. Reloading is implemented by
-removing them all from their respective classes and modules using
-`Module#remove_const`. That way, when the code goes on, those constants are
-going to be unknown again, and files reloaded on demand.
-
-INFO. This is an all-or-nothing operation, Rails does not attempt to reload only
-what changed since dependencies between classes makes that really tricky.
-Instead, everything is wiped.
-
-
-Module#autoload isn't Involved
-------------------------------
-
-`Module#autoload` provides a lazy way to load constants that is fully integrated
-with the Ruby constant lookup algorithms, dynamic constant API, etc. It is quite
-transparent.
-
-Rails internals make extensive use of it to defer as much work as possible from
-the boot process. But constant autoloading in Rails is **not** implemented with
-`Module#autoload`.
-
-One possible implementation based on `Module#autoload` would be to walk the
-application tree and issue `autoload` calls that map existing file names to
-their conventional constant name.
-
-There are a number of reasons that prevent Rails from using that implementation.
-
-For example, `Module#autoload` is only capable of loading files using `require`,
-so reloading would not be possible. Not only that, it uses an internal `require`
-which is not `Kernel#require`.
-
-Then, it provides no way to remove declarations in case a file is deleted. If a
-constant gets removed with `Module#remove_const` its `autoload` is not triggered
-again. Also, it doesn't support qualified names, so files with namespaces should
-be interpreted during the walk tree to install their own `autoload` calls, but
-those files could have constant references not yet configured.
-
-An implementation based on `Module#autoload` would be awesome but, as you see,
-at least as of today it is not possible. Constant autoloading in Rails is
-implemented with `Module#const_missing`, and that's why it has its own contract,
-documented in this guide.
-
-
-Common Gotchas
---------------
-
-### Nesting and Qualified Constants
-
-Let's consider
-
-```ruby
-module Admin
- class UsersController < ApplicationController
- def index
- @users = User.all
+ # Constantizes all types present in the database. There might be more on
+ # disk, but that does not matter in practice as far as the STI API is
+ # concerned.
+ #
+ # Assumes store_full_sti_class is true, the default.
+ def preload_sti
+ types_in_db = \
+ base_class.
+ select(inheritance_column).
+ distinct.
+ pluck(inheritance_column).
+ compact.
+ each(&:constantize)
+
+ types_in_db.each do |type|
+ logger.debug("Preloading STI type #{type}")
+ type.constantize
+ end
+
+ self.preloaded = true
+ end
end
end
end
```
-and
+and then include it in the STI root classes of your project:
```ruby
-class Admin::UsersController < ApplicationController
- def index
- @users = User.all
- end
-end
-```
-
-To resolve `User` Ruby checks `Admin` in the former case, but it does not in
-the latter because it does not belong to the nesting (see [Nesting](#nesting)
-and [Resolution Algorithms](#resolution-algorithms)).
-
-Unfortunately Rails autoloading does not know the nesting in the spot where the
-constant was missing and so it is not able to act as Ruby would. In particular,
-`Admin::User` will get autoloaded in either case.
-
-Albeit qualified constants with `class` and `module` keywords may technically
-work with autoloading in some cases, it is preferable to use relative constants
-instead:
+# app/models/shape.rb
+require "sti_preload"
-```ruby
-module Admin
- class UsersController < ApplicationController
- def index
- @users = User.all
- end
- end
+class Shape < ApplicationRecord
+ include StiPreload # Only in the root class.
end
-```
-### Autoloading and STI
-
-Single Table Inheritance (STI) is a feature of Active Record that enables
-storing a hierarchy of models in one single table. The API of such models is
-aware of the hierarchy and encapsulates some common needs. For example, given
-these classes:
-
-```ruby
# app/models/polygon.rb
-class Polygon < ApplicationRecord
+class Polygon < Shape
end
# app/models/triangle.rb
class Triangle < Polygon
end
-
-# app/models/rectangle.rb
-class Rectangle < Polygon
-end
-```
-
-`Triangle.create` creates a row that represents a triangle, and
-`Rectangle.create` creates a row that represents a rectangle. If `id` is the
-ID of an existing record, `Polygon.find(id)` returns an object of the correct
-type.
-
-Methods that operate on collections are also aware of the hierarchy. For
-example, `Polygon.all` returns all the records of the table, because all
-rectangles and triangles are polygons. Active Record takes care of returning
-instances of their corresponding class in the result set.
-
-Types are autoloaded as needed. For example, if `Polygon.first` is a rectangle
-and `Rectangle` has not yet been loaded, Active Record autoloads it and the
-record is correctly instantiated.
-
-All good, but if instead of performing queries based on the root class we need
-to work on some subclass, things get interesting.
-
-While working with `Polygon` you do not need to be aware of all its descendants,
-because anything in the table is by definition a polygon, but when working with
-subclasses Active Record needs to be able to enumerate the types it is looking
-for. Let's see an example.
-
-`Rectangle.all` only loads rectangles by adding a type constraint to the query:
-
-```sql
-SELECT "polygons".* FROM "polygons"
-WHERE "polygons"."type" IN ("Rectangle")
-```
-
-Let's introduce now a subclass of `Rectangle`:
-
-```ruby
-# app/models/square.rb
-class Square < Rectangle
-end
-```
-
-`Rectangle.all` should now return rectangles **and** squares:
-
-```sql
-SELECT "polygons".* FROM "polygons"
-WHERE "polygons"."type" IN ("Rectangle", "Square")
-```
-
-But there's a caveat here: How does Active Record know that the class `Square`
-exists at all?
-
-Even if the file `app/models/square.rb` exists and defines the `Square` class,
-if no code yet used that class, `Rectangle.all` issues the query
-
-```sql
-SELECT "polygons".* FROM "polygons"
-WHERE "polygons"."type" IN ("Rectangle")
-```
-
-That is not a bug, the query includes all *known* descendants of `Rectangle`.
-
-A way to ensure this works correctly regardless of the order of execution is to
-manually load the direct subclasses at the bottom of the file that defines each
-intermediate class:
-
-```ruby
-# app/models/rectangle.rb
-class Rectangle < Polygon
-end
-require_dependency 'square'
-```
-
-This needs to happen for every intermediate (non-root and non-leaf) class. The
-root class does not scope the query by type, and therefore does not necessarily
-have to know all its descendants.
-
-### Autoloading and `require`
-
-Files defining constants to be autoloaded should never be `require`d:
-
-```ruby
-require 'user' # DO NOT DO THIS
-
-class UsersController < ApplicationController
- ...
-end
-```
-
-There are two possible gotchas here in development mode:
-
-1. If `User` is autoloaded before reaching the `require`, `app/models/user.rb`
-runs again because `load` does not update `$LOADED_FEATURES`.
-
-2. If the `require` runs first Rails does not mark `User` as an autoloaded
-constant and changes to `app/models/user.rb` aren't reloaded.
-
-Just follow the flow and use constant autoloading always, never mix
-autoloading and `require`. As a last resort, if some file absolutely needs to
-load a certain file use `require_dependency` to play nice with constant
-autoloading. This option is rarely needed in practice, though.
-
-Of course, using `require` in autoloaded files to load ordinary 3rd party
-libraries is fine, and Rails is able to distinguish their constants, they are
-not marked as autoloaded.
-
-### Autoloading and Initializers
-
-Consider this assignment in `config/initializers/set_auth_service.rb`:
-
-```ruby
-AUTH_SERVICE = if Rails.env.production?
- RealAuthService
-else
- MockedAuthService
-end
-```
-
-The purpose of this setup would be that the application uses the class that
-corresponds to the environment via `AUTH_SERVICE`. In development mode
-`MockedAuthService` gets autoloaded when the initializer runs. Let's suppose
-we do some requests, change its implementation, and hit the application again.
-To our surprise the changes are not reflected. Why?
-
-As [we saw earlier](#constant-reloading), Rails removes autoloaded constants,
-but `AUTH_SERVICE` stores the original class object. Stale, non-reachable
-using the original constant, but perfectly functional.
-
-The following code summarizes the situation:
-
-```ruby
-class C
- def quack
- 'quack!'
- end
-end
-
-X = C
-Object.instance_eval { remove_const(:C) }
-X.new.quack # => quack!
-X.name # => C
-C # => uninitialized constant C (NameError)
-```
-
-Because of that, it is not a good idea to autoload constants on application
-initialization.
-
-In the case above we could implement a dynamic access point:
-
-```ruby
-# app/models/auth_service.rb
-class AuthService
- if Rails.env.production?
- def self.instance
- RealAuthService
- end
- else
- def self.instance
- MockedAuthService
- end
- end
-end
-```
-
-and have the application use `AuthService.instance` instead. `AuthService`
-would be loaded on demand and be autoload-friendly.
-
-### `require_dependency` and Initializers
-
-As we saw before, `require_dependency` loads files in an autoloading-friendly
-way. Normally, though, such a call does not make sense in an initializer.
-
-One could think about doing some [`require_dependency`](#require-dependency)
-calls in an initializer to make sure certain constants are loaded upfront, for
-example as an attempt to address the [gotcha with STIs](#autoloading-and-sti).
-
-Problem is, in development mode [autoloaded constants are wiped](#constant-reloading)
-if there is any relevant change in the file system. If that happens then
-we are in the very same situation the initializer wanted to avoid!
-
-Calls to `require_dependency` have to be strategically written in autoloaded
-spots.
-
-### When Constants aren't Missed
-
-#### Relative References
-
-Let's consider a flight simulator. The application has a default flight model
-
-```ruby
-# app/models/flight_model.rb
-class FlightModel
-end
-```
-
-that can be overridden by each airplane, for instance
-
-```ruby
-# app/models/bell_x1/flight_model.rb
-module BellX1
- class FlightModel < FlightModel
- end
-end
-
-# app/models/bell_x1/aircraft.rb
-module BellX1
- class Aircraft
- def initialize
- @flight_model = FlightModel.new
- end
- end
-end
-```
-
-The initializer wants to create a `BellX1::FlightModel` and nesting has
-`BellX1`, that looks good. But if the default flight model is loaded and the
-one for the Bell-X1 is not, the interpreter is able to resolve the top-level
-`FlightModel` and autoloading is thus not triggered for `BellX1::FlightModel`.
-
-That code depends on the execution path.
-
-These kind of ambiguities can often be resolved using qualified constants:
-
-```ruby
-module BellX1
- class Plane
- def flight_model
- @flight_model ||= BellX1::FlightModel.new
- end
- end
-end
-```
-
-Also, `require_dependency` is a solution:
-
-```ruby
-require_dependency 'bell_x1/flight_model'
-
-module BellX1
- class Plane
- def flight_model
- @flight_model ||= FlightModel.new
- end
- end
-end
-```
-
-#### Qualified References
-
-WARNING. This gotcha is only possible in Ruby < 2.5.
-
-Given
-
-```ruby
-# app/models/hotel.rb
-class Hotel
-end
-
-# app/models/image.rb
-class Image
-end
-
-# app/models/hotel/image.rb
-class Hotel
- class Image < Image
- end
-end
-```
-
-the expression `Hotel::Image` is ambiguous because it depends on the execution
-path.
-
-As [we saw before](#resolution-algorithm-for-qualified-constants), Ruby looks
-up the constant in `Hotel` and its ancestors. If `app/models/image.rb` has
-been loaded but `app/models/hotel/image.rb` hasn't, Ruby does not find `Image`
-in `Hotel`, but it does in `Object`:
-
-```
-$ rails r 'Image; p Hotel::Image' 2>/dev/null
-Image # NOT Hotel::Image!
-```
-
-The code evaluating `Hotel::Image` needs to make sure
-`app/models/hotel/image.rb` has been loaded, possibly with
-`require_dependency`.
-
-In these cases the interpreter issues a warning though:
-
-```
-warning: toplevel constant Image referenced by Hotel::Image
-```
-
-This surprising constant resolution can be observed with any qualifying class:
-
-```
-2.1.5 :001 > String::Array
-(irb):1: warning: toplevel constant Array referenced by String::Array
- => Array
-```
-
-WARNING. To find this gotcha the qualifying namespace has to be a class,
-`Object` is not an ancestor of modules.
-
-### Autoloading within Singleton Classes
-
-Let's suppose we have these class definitions:
-
-```ruby
-# app/models/hotel/services.rb
-module Hotel
- class Services
- end
-end
-
-# app/models/hotel/geo_location.rb
-module Hotel
- class GeoLocation
- class << self
- Services
- end
- end
-end
```
-If `Hotel::Services` is known by the time `app/models/hotel/geo_location.rb`
-is being loaded, `Services` is resolved by Ruby because `Hotel` belongs to the
-nesting when the singleton class of `Hotel::GeoLocation` is opened.
+Rails.autoloaders
+-----------------
-But if `Hotel::Services` is not known, Rails is not able to autoload it, the
-application raises `NameError`.
-
-The reason is that autoloading is triggered for the singleton class, which is
-anonymous, and as [we saw before](#generic-procedure), Rails only checks the
-top-level namespace in that edge case.
-
-An easy solution to this caveat is to qualify the constant:
+The Zeitwerk instances managing your application are availabe at
```ruby
-module Hotel
- class GeoLocation
- class << self
- Hotel::Services
- end
- end
-end
+Rails.autoloaders.main
+Rails.autoloaders.once
```
-### Autoloading in `BasicObject`
-
-Direct descendants of `BasicObject` do not have `Object` among their ancestors
-and cannot resolve top-level constants:
-
-```ruby
-class C < BasicObject
- String # NameError: uninitialized constant C::String
-end
-```
+The former is the main one. The latter is there mostly for backwards compatibility reasons, in case the application has something in `config.autoload_once_paths` (this is discouraged nowadays).
-When autoloading is involved that plot has a twist. Let's consider:
+You can check if `zeitwerk` mode is enabled with
```ruby
-class C < BasicObject
- def user
- User # WRONG
- end
-end
+Rails.autoloaders.zeitwerk_enabled?
```
-Since Rails checks the top-level namespace `User` gets autoloaded just fine the
-first time the `user` method is invoked. You only get the exception if the
-`User` constant is known at that point, in particular in a *second* call to
-`user`:
-
-```ruby
-c = C.new
-c.user # surprisingly fine, User
-c.user # NameError: uninitialized constant C::User
-```
-
-because it detects that a parent namespace already has the constant (see [Qualified
-References](#autoloading-algorithms-qualified-references)).
-
-As with pure Ruby, within the body of a direct descendant of `BasicObject` use
-always absolute constant paths:
-
-```ruby
-class C < BasicObject
- ::String # RIGHT
-
- def user
- ::User # RIGHT
- end
-end
-```
-
-### Autoloading in the Test Environment
-
-When configuring the `test` environment for autoloading you might consider multiple factors.
-
-For example it might be worth running your tests with an identical setup to production (`config.eager_load = true`, `config.cache_classes = true`) in order to catch any problems before they hit production (this is compensation for the lack of dev-prod parity). However this will slow down the boot time for individual tests on a dev machine (and is not immediately compatible with spring see below). So one possibility is to do this on a
-[CI](https://en.wikipedia.org/wiki/Continuous_integration) machine only (which should run without spring).
-
-On a development machine you can then have your tests running with whatever is fastest (ideally `config.eager_load = false`).
-
-With the [Spring](https://github.com/rails/spring) pre-loader (included with new Rails apps), you ideally keep `config.eager_load = false` as per development. Sometimes you may end up with a hybrid configuration (`config.eager_load = true`, `config.cache_classes = true` AND `config.enable_dependency_loading = true`), see [spring issue](https://github.com/rails/spring/issues/519#issuecomment-348324369). However it might be simpler to keep the same configuration as development, and work out whatever it is that is causing autoloading to fail (perhaps by the results of your CI test results).
-
-Occasionally you may need to explicitly eager_load by using `Rails
-.application.eager_load!` in the setup of your tests -- this might occur if your [tests involve multithreading](https://stackoverflow.com/questions/25796409/in-rails-how-can-i-eager-load-all-code-before-a-specific-rspec-test).
-
-## Troubleshooting
-
-### Tracing Autoloads
-
-Active Support is able to report constants as they are autoloaded. To enable these traces in a Rails application, put the following two lines in some initializer:
-
-```ruby
-ActiveSupport::Dependencies.logger = Rails.logger
-ActiveSupport::Dependencies.verbose = true
-```
-
-### Where is a Given Autoload Triggered?
+Opting Out
+----------
-If constant `Foo` is being autoloaded, and you'd like to know where is that autoload coming from, just throw
+Applications can load Rails 6 defaults and still use the classic autoloader this way:
```ruby
-puts caller
+# config/application.rb
+config.load_defaults "6.x"
+config.autoloader = :classic
```
-at the top of `foo.rb` and inspect the printed stack trace.
-
-### Which Constants Have Been Autoloaded?
-
-At any given time,
-
-```ruby
-ActiveSupport::Dependencies.autoloaded_constants
-```
+That may be handy if upgrading to Rails 6 in different phases, but classic mode is discouraged for new applications.
-has the collection of constants that have been autoloaded so far.
+`zeitwerk` mode is not available in versions of Rails previous to 6.0.
diff --git a/guides/source/autoloading_and_reloading_constants_classic_mode.md b/guides/source/autoloading_and_reloading_constants_classic_mode.md
new file mode 100644
index 0000000000..d0d9e076d6
--- /dev/null
+++ b/guides/source/autoloading_and_reloading_constants_classic_mode.md
@@ -0,0 +1,1351 @@
+**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**
+
+Autoloading and Reloading Constants (Classic Mode)
+==================================================
+
+This guide documents how constant autoloading and reloading works in `classic` mode.
+
+After reading this guide, you will know:
+
+* Key aspects of Ruby constants
+* What are the `autoload_paths` and how does eager loading work in production?
+* How constant autoloading works
+* What is `require_dependency`
+* How constant reloading works
+* Solutions to common autoloading gotchas
+
+--------------------------------------------------------------------------------
+
+
+Introduction
+------------
+
+INFO. This guide documents autoloading in `classic` mode, which is the traditional one. If you'd like to read about `zeiwerk` mode instead, the new one in Rails 6, please check [Autoloading and Reloading Constants (Zeitwerk Mode)](autoloading_and_reloading_constants.html).
+
+Ruby on Rails allows applications to be written as if their code was preloaded.
+
+In a normal Ruby program classes need to load their dependencies:
+
+```ruby
+require 'application_controller'
+require 'post'
+
+class PostsController < ApplicationController
+ def index
+ @posts = Post.all
+ end
+end
+```
+
+Our Rubyist instinct quickly sees some redundancy in there: If classes were
+defined in files matching their name, couldn't their loading be automated
+somehow? We could save scanning the file for dependencies, which is brittle.
+
+Moreover, `Kernel#require` loads files once, but development is much more smooth
+if code gets refreshed when it changes without restarting the server. It would
+be nice to be able to use `Kernel#load` in development, and `Kernel#require` in
+production.
+
+Indeed, those features are provided by Ruby on Rails, where we just write
+
+```ruby
+class PostsController < ApplicationController
+ def index
+ @posts = Post.all
+ end
+end
+```
+
+This guide documents how that works.
+
+
+Constants Refresher
+-------------------
+
+While constants are trivial in most programming languages, they are a rich
+topic in Ruby.
+
+It is beyond the scope of this guide to document Ruby constants, but we are
+nevertheless going to highlight a few key topics. Truly grasping the following
+sections is instrumental to understanding constant autoloading and reloading.
+
+### Nesting
+
+Class and module definitions can be nested to create namespaces:
+
+```ruby
+module XML
+ class SAXParser
+ # (1)
+ end
+end
+```
+
+The *nesting* at any given place is the collection of enclosing nested class and
+module objects outwards. The nesting at any given place can be inspected with
+`Module.nesting`. For example, in the previous example, the nesting at
+(1) is
+
+```ruby
+[XML::SAXParser, XML]
+```
+
+It is important to understand that the nesting is composed of class and module
+*objects*, it has nothing to do with the constants used to access them, and is
+also unrelated to their names.
+
+For instance, while this definition is similar to the previous one:
+
+```ruby
+class XML::SAXParser
+ # (2)
+end
+```
+
+the nesting in (2) is different:
+
+```ruby
+[XML::SAXParser]
+```
+
+`XML` does not belong to it.
+
+We can see in this example that the name of a class or module that belongs to a
+certain nesting does not necessarily correlate with the namespaces at the spot.
+
+Even more, they are totally independent, take for instance
+
+```ruby
+module X
+ module Y
+ end
+end
+
+module A
+ module B
+ end
+end
+
+module X::Y
+ module A::B
+ # (3)
+ end
+end
+```
+
+The nesting in (3) consists of two module objects:
+
+```ruby
+[A::B, X::Y]
+```
+
+So, it not only doesn't end in `A`, which does not even belong to the nesting,
+but it also contains `X::Y`, which is independent from `A::B`.
+
+The nesting is an internal stack maintained by the interpreter, and it gets
+modified according to these rules:
+
+* The class object following a `class` keyword gets pushed when its body is
+executed, and popped after it.
+
+* The module object following a `module` keyword gets pushed when its body is
+executed, and popped after it.
+
+* A singleton class opened with `class << object` gets pushed, and popped later.
+
+* When `instance_eval` is called using a string argument,
+the singleton class of the receiver is pushed to the nesting of the eval'ed
+code. When `class_eval` or `module_eval` is called using a string argument,
+the receiver is pushed to the nesting of the eval'ed code.
+
+* The nesting at the top-level of code interpreted by `Kernel#load` is empty
+unless the `load` call receives a true value as second argument, in which case
+a newly created anonymous module is pushed by Ruby.
+
+It is interesting to observe that blocks do not modify the stack. In particular
+the blocks that may be passed to `Class.new` and `Module.new` do not get the
+class or module being defined pushed to their nesting. That's one of the
+differences between defining classes and modules in one way or another.
+
+### Class and Module Definitions are Constant Assignments
+
+Let's suppose the following snippet creates a class (rather than reopening it):
+
+```ruby
+class C
+end
+```
+
+Ruby creates a constant `C` in `Object` and stores in that constant a class
+object. The name of the class instance is "C", a string, named after the
+constant.
+
+That is,
+
+```ruby
+class Project < ApplicationRecord
+end
+```
+
+performs a constant assignment equivalent to
+
+```ruby
+Project = Class.new(ApplicationRecord)
+```
+
+including setting the name of the class as a side-effect:
+
+```ruby
+Project.name # => "Project"
+```
+
+Constant assignment has a special rule to make that happen: if the object
+being assigned is an anonymous class or module, Ruby sets the object's name to
+the name of the constant.
+
+INFO. From then on, what happens to the constant and the instance does not
+matter. For example, the constant could be deleted, the class object could be
+assigned to a different constant, be stored in no constant anymore, etc. Once
+the name is set, it doesn't change.
+
+Similarly, module creation using the `module` keyword as in
+
+```ruby
+module Admin
+end
+```
+
+performs a constant assignment equivalent to
+
+```ruby
+Admin = Module.new
+```
+
+including setting the name as a side-effect:
+
+```ruby
+Admin.name # => "Admin"
+```
+
+WARNING. The execution context of a block passed to `Class.new` or `Module.new`
+is not entirely equivalent to the one of the body of the definitions using the
+`class` and `module` keywords. But both idioms result in the same constant
+assignment.
+
+Thus, an informal expression like "the `String` class" technically means the
+class object stored in the constant called "String". That constant, in turn,
+belongs to the class object stored in the constant called "Object".
+
+`String` is an ordinary constant, and everything related to them such as
+resolution algorithms applies to it.
+
+Likewise, in the controller
+
+```ruby
+class PostsController < ApplicationController
+ def index
+ @posts = Post.all
+ end
+end
+```
+
+`Post` is not syntax for a class. Rather, `Post` is a regular Ruby constant. If
+all is good, the constant is evaluated to an object that responds to `all`.
+
+That is why we talk about *constant* autoloading, Rails has the ability to
+load constants on the fly.
+
+### Constants are Stored in Modules
+
+Constants belong to modules in a very literal sense. Classes and modules have
+a constant table; think of it as a hash table.
+
+Let's analyze an example to really understand what that means. While common
+abuses of language like "the `String` class" are convenient, the exposition is
+going to be precise here for didactic purposes.
+
+Let's consider the following module definition:
+
+```ruby
+module Colors
+ RED = '0xff0000'
+end
+```
+
+First, when the `module` keyword is processed, the interpreter creates a new
+entry in the constant table of the class object stored in the `Object` constant.
+Said entry associates the name "Colors" to a newly created module object.
+Furthermore, the interpreter sets the name of the new module object to be the
+string "Colors".
+
+Later, when the body of the module definition is interpreted, a new entry is
+created in the constant table of the module object stored in the `Colors`
+constant. That entry maps the name "RED" to the string "0xff0000".
+
+In particular, `Colors::RED` is totally unrelated to any other `RED` constant
+that may live in any other class or module object. If there were any, they
+would have separate entries in their respective constant tables.
+
+Pay special attention in the previous paragraphs to the distinction between
+class and module objects, constant names, and value objects associated to them
+in constant tables.
+
+### Resolution Algorithms
+
+#### Resolution Algorithm for Relative Constants
+
+At any given place in the code, let's define *cref* to be the first element of
+the nesting if it is not empty, or `Object` otherwise.
+
+Without getting too much into the details, the resolution algorithm for relative
+constant references goes like this:
+
+1. If the nesting is not empty the constant is looked up in its elements and in
+order. The ancestors of those elements are ignored.
+
+2. If not found, then the algorithm walks up the ancestor chain of the cref.
+
+3. If not found and the cref is a module, the constant is looked up in `Object`.
+
+4. If not found, `const_missing` is invoked on the cref. The default
+implementation of `const_missing` raises `NameError`, but it can be overridden.
+
+Rails autoloading **does not emulate this algorithm**, but its starting point is
+the name of the constant to be autoloaded, and the cref. See more in [Relative
+References](#autoloading-algorithms-relative-references).
+
+#### Resolution Algorithm for Qualified Constants
+
+Qualified constants look like this:
+
+```ruby
+Billing::Invoice
+```
+
+`Billing::Invoice` is composed of two constants: `Billing` is relative and is
+resolved using the algorithm of the previous section.
+
+INFO. Leading colons would make the first segment absolute rather than
+relative: `::Billing::Invoice`. That would force `Billing` to be looked up
+only as a top-level constant.
+
+`Invoice` on the other hand is qualified by `Billing` and we are going to see
+its resolution next. Let's define *parent* to be that qualifying class or module
+object, that is, `Billing` in the example above. The algorithm for qualified
+constants goes like this:
+
+1. The constant is looked up in the parent and its ancestors. In Ruby >= 2.5,
+`Object` is skipped if present among the ancestors. `Kernel` and `BasicObject`
+are still checked though.
+
+2. If the lookup fails, `const_missing` is invoked in the parent. The default
+implementation of `const_missing` raises `NameError`, but it can be overridden.
+
+INFO. In Ruby < 2.5 `String::Hash` evaluates to `Hash` and the interpreter
+issues a warning: "toplevel constant Hash referenced by String::Hash". Starting
+with 2.5, `String::Hash` raises `NameError` because `Object` is skipped.
+
+As you see, this algorithm is simpler than the one for relative constants. In
+particular, the nesting plays no role here, and modules are not special-cased,
+if neither they nor their ancestors have the constants, `Object` is **not**
+checked.
+
+Rails autoloading **does not emulate this algorithm**, but its starting point is
+the name of the constant to be autoloaded, and the parent. See more in
+[Qualified References](#autoloading-algorithms-qualified-references).
+
+
+Vocabulary
+----------
+
+### Parent Namespaces
+
+Given a string with a constant path we define its *parent namespace* to be the
+string that results from removing its rightmost segment.
+
+For example, the parent namespace of the string "A::B::C" is the string "A::B",
+the parent namespace of "A::B" is "A", and the parent namespace of "A" is "".
+
+The interpretation of a parent namespace when thinking about classes and modules
+is tricky though. Let's consider a module M named "A::B":
+
+* The parent namespace, "A", may not reflect nesting at a given spot.
+
+* The constant `A` may no longer exist, some code could have removed it from
+`Object`.
+
+* If `A` exists, the class or module that was originally in `A` may not be there
+anymore. For example, if after a constant removal there was another constant
+assignment there would generally be a different object in there.
+
+* In such case, it could even happen that the reassigned `A` held a new class or
+module called also "A"!
+
+* In the previous scenarios M would no longer be reachable through `A::B` but
+the module object itself could still be alive somewhere and its name would
+still be "A::B".
+
+The idea of a parent namespace is at the core of the autoloading algorithms
+and helps explain and understand their motivation intuitively, but as you see
+that metaphor leaks easily. Given an edge case to reason about, take always into
+account that by "parent namespace" the guide means exactly that specific string
+derivation.
+
+### Loading Mechanism
+
+Rails autoloads files with `Kernel#load` when `config.cache_classes` is false,
+the default in development mode, and with `Kernel#require` otherwise, the
+default in production mode.
+
+`Kernel#load` allows Rails to execute files more than once if [constant
+reloading](#constant-reloading) is enabled.
+
+This guide uses the word "load" freely to mean a given file is interpreted, but
+the actual mechanism can be `Kernel#load` or `Kernel#require` depending on that
+flag.
+
+
+Autoloading Availability
+------------------------
+
+Rails is always able to autoload provided its environment is in place. For
+example the `runner` command autoloads:
+
+```
+$ rails runner 'p User.column_names'
+["id", "email", "created_at", "updated_at"]
+```
+
+The console autoloads, the test suite autoloads, and of course the application
+autoloads.
+
+By default, Rails eager loads the application files when it boots in production
+mode, so most of the autoloading going on in development does not happen. But
+autoloading may still be triggered during eager loading.
+
+For example, given
+
+```ruby
+class BeachHouse < House
+end
+```
+
+if `House` is still unknown when `app/models/beach_house.rb` is being eager
+loaded, Rails autoloads it.
+
+
+autoload_paths and eager_load_paths
+-----------------------------------
+
+As you probably know, when `require` gets a relative file name:
+
+```ruby
+require 'erb'
+```
+
+Ruby looks for the file in the directories listed in `$LOAD_PATH`. That is, Ruby
+iterates over all its directories and for each one of them checks whether they
+have a file called "erb.rb", or "erb.so", or "erb.o", or "erb.dll". If it finds
+any of them, the interpreter loads it and ends the search. Otherwise, it tries
+again in the next directory of the list. If the list gets exhausted, `LoadError`
+is raised.
+
+We are going to cover how constant autoloading works in more detail later, but
+the idea is that when a constant like `Post` is hit and missing, if there's a
+`post.rb` file for example in `app/models` Rails is going to find it, evaluate
+it, and have `Post` defined as a side-effect.
+
+All right, Rails has a collection of directories similar to `$LOAD_PATH` in which
+to look up `post.rb`. That collection is called `autoload_paths` and by
+default it contains:
+
+* All subdirectories of `app` in the application and engines present at boot
+ time. For example, `app/controllers`. They do not need to be the default
+ ones, any custom directories like `app/workers` belong automatically to
+ `autoload_paths`.
+
+* Any existing second level directories called `app/*/concerns` in the
+ application and engines.
+
+* The directory `test/mailers/previews`.
+
+`eager_load_paths` is initially the `app` paths above
+
+How files are autoloaded depends on `eager_load` and `cache_classes` config settings which typically vary in development, production, and test modes:
+
+ * In **development**, you want quicker startup with incremental loading of application code. So `eager_load` should be set to `false`, and Rails will autoload files as needed (see [Autoloading Algorithms](#autoloading-algorithms) below) -- and then reload them when they change (see [Constant Reloading](#constant-reloading) below).
+ * In **production**, however, you want consistency and thread-safety and can live with a longer boot time. So `eager_load` is set to `true`, and then during boot (before the app is ready to receive requests) Rails loads all files in the `eager_load_paths` and then turns off auto loading (NB: autoloading may be needed during eager loading). Not autoloading after boot is a `good thing`, as autoloading can cause the app to be have thread-safety problems.
+ * In **test**, for speed of execution (of individual tests) `eager_load` is `false`, so Rails follows development behaviour.
+
+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).
+
+`config.autoload_paths` is not changeable from environment-specific configuration files.
+
+The value of `autoload_paths` can be inspected. In a just-generated application
+it is (edited):
+
+```
+$ rails r 'puts ActiveSupport::Dependencies.autoload_paths'
+.../app/assets
+.../app/channels
+.../app/controllers
+.../app/controllers/concerns
+.../app/helpers
+.../app/jobs
+.../app/mailers
+.../app/models
+.../app/models/concerns
+.../activestorage/app/assets
+.../activestorage/app/controllers
+.../activestorage/app/javascript
+.../activestorage/app/jobs
+.../activestorage/app/models
+.../actioncable/app/assets
+.../actionview/app/assets
+.../test/mailers/previews
+```
+
+INFO. `autoload_paths` is computed and cached during the initialization process.
+The application needs to be restarted to reflect any changes in the directory
+structure.
+
+
+Autoloading Algorithms
+----------------------
+
+### Relative References
+
+A relative constant reference may appear in several places, for example, in
+
+```ruby
+class PostsController < ApplicationController
+ def index
+ @posts = Post.all
+ end
+end
+```
+
+all three constant references are relative.
+
+#### Constants after the `class` and `module` Keywords
+
+Ruby performs a lookup for the constant that follows a `class` or `module`
+keyword because it needs to know if the class or module is going to be created
+or reopened.
+
+If the constant is not defined at that point it is not considered to be a
+missing constant, autoloading is **not** triggered.
+
+So, in the previous example, if `PostsController` is not defined when the file
+is interpreted Rails autoloading is not going to be triggered, Ruby will just
+define the controller.
+
+#### Top-Level Constants
+
+On the contrary, if `ApplicationController` is unknown, the constant is
+considered missing and an autoload is going to be attempted by Rails.
+
+In order to load `ApplicationController`, Rails iterates over `autoload_paths`.
+First it checks if `app/assets/application_controller.rb` exists. If it does not,
+which is normally the case, it continues and finds
+`app/controllers/application_controller.rb`.
+
+If the file defines the constant `ApplicationController` all is fine, otherwise
+`LoadError` is raised:
+
+```
+unable to autoload constant ApplicationController, expected
+<full path to application_controller.rb> to define it (LoadError)
+```
+
+INFO. Rails does not require the value of autoloaded constants to be a class or
+module object. For example, if the file `app/models/max_clients.rb` defines
+`MAX_CLIENTS = 100` autoloading `MAX_CLIENTS` works just fine.
+
+#### Namespaces
+
+Autoloading `ApplicationController` looks directly under the directories of
+`autoload_paths` because the nesting in that spot is empty. The situation of
+`Post` is different, the nesting in that line is `[PostsController]` and support
+for namespaces comes into play.
+
+The basic idea is that given
+
+```ruby
+module Admin
+ class BaseController < ApplicationController
+ @@all_roles = Role.all
+ end
+end
+```
+
+to autoload `Role` we are going to check if it is defined in the current or
+parent namespaces, one at a time. So, conceptually we want to try to autoload
+any of
+
+```
+Admin::BaseController::Role
+Admin::Role
+Role
+```
+
+in that order. That's the idea. To do so, Rails looks in `autoload_paths`
+respectively for file names like these:
+
+```
+admin/base_controller/role.rb
+admin/role.rb
+role.rb
+```
+
+modulus some additional directory lookups we are going to cover soon.
+
+INFO. `'Constant::Name'.underscore` gives the relative path without extension of
+the file name where `Constant::Name` is expected to be defined.
+
+Let's see how Rails autoloads the `Post` constant in the `PostsController`
+above assuming the application has a `Post` model defined in
+`app/models/post.rb`.
+
+First it checks for `posts_controller/post.rb` in `autoload_paths`:
+
+```
+app/assets/posts_controller/post.rb
+app/controllers/posts_controller/post.rb
+app/helpers/posts_controller/post.rb
+...
+test/mailers/previews/posts_controller/post.rb
+```
+
+Since the lookup is exhausted without success, a similar search for a directory
+is performed, we are going to see why in the [next section](#automatic-modules):
+
+```
+app/assets/posts_controller/post
+app/controllers/posts_controller/post
+app/helpers/posts_controller/post
+...
+test/mailers/previews/posts_controller/post
+```
+
+If all those attempts fail, then Rails starts the lookup again in the parent
+namespace. In this case only the top-level remains:
+
+```
+app/assets/post.rb
+app/controllers/post.rb
+app/helpers/post.rb
+app/mailers/post.rb
+app/models/post.rb
+```
+
+A matching file is found in `app/models/post.rb`. The lookup stops there and the
+file is loaded. If the file actually defines `Post` all is fine, otherwise
+`LoadError` is raised.
+
+### Qualified References
+
+When a qualified constant is missing Rails does not look for it in the parent
+namespaces. But there is a caveat: when a constant is missing, Rails is
+unable to tell if the trigger was a relative reference or a qualified one.
+
+For example, consider
+
+```ruby
+module Admin
+ User
+end
+```
+
+and
+
+```ruby
+Admin::User
+```
+
+If `User` is missing, in either case all Rails knows is that a constant called
+"User" was missing in a module called "Admin".
+
+If there is a top-level `User` Ruby would resolve it in the former example, but
+wouldn't in the latter. In general, Rails does not emulate the Ruby constant
+resolution algorithms, but in this case it tries using the following heuristic:
+
+> If none of the parent namespaces of the class or module has the missing
+> constant then Rails assumes the reference is relative. Otherwise qualified.
+
+For example, if this code triggers autoloading
+
+```ruby
+Admin::User
+```
+
+and the `User` constant is already present in `Object`, it is not possible that
+the situation is
+
+```ruby
+module Admin
+ User
+end
+```
+
+because otherwise Ruby would have resolved `User` and no autoloading would have
+been triggered in the first place. Thus, Rails assumes a qualified reference and
+considers the file `admin/user.rb` and directory `admin/user` to be the only
+valid options.
+
+In practice, this works quite well as long as the nesting matches all parent
+namespaces respectively and the constants that make the rule apply are known at
+that time.
+
+However, autoloading happens on demand. If by chance the top-level `User` was
+not yet loaded, then Rails assumes a relative reference by contract.
+
+Naming conflicts of this kind are rare in practice, but if one occurs,
+`require_dependency` provides a solution by ensuring that the constant needed
+to trigger the heuristic is defined in the conflicting place.
+
+### Automatic Modules
+
+When a module acts as a namespace, Rails does not require the application to
+define a file for it, a directory matching the namespace is enough.
+
+Suppose an application has a back office whose controllers are stored in
+`app/controllers/admin`. If the `Admin` module is not yet loaded when
+`Admin::UsersController` is hit, Rails needs first to autoload the constant
+`Admin`.
+
+If `autoload_paths` has a file called `admin.rb` Rails is going to load that
+one, but if there's no such file and a directory called `admin` is found, Rails
+creates an empty module and assigns it to the `Admin` constant on the fly.
+
+### Generic Procedure
+
+Relative references are reported to be missing in the cref where they were hit,
+and qualified references are reported to be missing in their parent (see
+[Resolution Algorithm for Relative
+Constants](#resolution-algorithm-for-relative-constants) at the beginning of
+this guide for the definition of *cref*, and [Resolution Algorithm for Qualified
+Constants](#resolution-algorithm-for-qualified-constants) for the definition of
+*parent*).
+
+The procedure to autoload constant `C` in an arbitrary situation is as follows:
+
+```
+if the class or module in which C is missing is Object
+ let ns = ''
+else
+ let M = the class or module in which C is missing
+
+ if M is anonymous
+ let ns = ''
+ else
+ let ns = M.name
+ end
+end
+
+loop do
+ # Look for a regular file.
+ for dir in autoload_paths
+ if the file "#{dir}/#{ns.underscore}/c.rb" exists
+ load/require "#{dir}/#{ns.underscore}/c.rb"
+
+ if C is now defined
+ return
+ else
+ raise LoadError
+ end
+ end
+ end
+
+ # Look for an automatic module.
+ for dir in autoload_paths
+ if the directory "#{dir}/#{ns.underscore}/c" exists
+ if ns is an empty string
+ let C = Module.new in Object and return
+ else
+ let C = Module.new in ns.constantize and return
+ end
+ end
+ end
+
+ if ns is empty
+ # We reached the top-level without finding the constant.
+ raise NameError
+ else
+ if C exists in any of the parent namespaces
+ # Qualified constants heuristic.
+ raise NameError
+ else
+ # Try again in the parent namespace.
+ let ns = the parent namespace of ns and retry
+ end
+ end
+end
+```
+
+
+require_dependency
+------------------
+
+Constant autoloading is triggered on demand and therefore code that uses a
+certain constant may have it already defined or may trigger an autoload. That
+depends on the execution path and it may vary between runs.
+
+There are times, however, in which you want to make sure a certain constant is
+known when the execution reaches some code. `require_dependency` provides a way
+to load a file using the current [loading mechanism](#loading-mechanism), and
+keeping track of constants defined in that file as if they were autoloaded to
+have them reloaded as needed.
+
+`require_dependency` is rarely needed, but see a couple of use cases in
+[Autoloading and STI](#autoloading-and-sti) and [When Constants aren't
+Triggered](#when-constants-aren-t-missed).
+
+WARNING. Unlike autoloading, `require_dependency` does not expect the file to
+define any particular constant. Exploiting this behavior would be a bad practice
+though, file and constant paths should match.
+
+
+Constant Reloading
+------------------
+
+When `config.cache_classes` is false Rails is able to reload autoloaded
+constants.
+
+For example, if you're in a console session and edit some file behind the
+scenes, the code can be reloaded with the `reload!` command:
+
+```
+> reload!
+```
+
+When the application runs, code is reloaded when something relevant to this
+logic changes. In order to do that, Rails monitors a number of things:
+
+* `config/routes.rb`.
+
+* Locales.
+
+* Ruby files under `autoload_paths`.
+
+* `db/schema.rb` and `db/structure.sql`.
+
+If anything in there changes, there is a middleware that detects it and reloads
+the code.
+
+Autoloading keeps track of autoloaded constants. Reloading is implemented by
+removing them all from their respective classes and modules using
+`Module#remove_const`. That way, when the code goes on, those constants are
+going to be unknown again, and files reloaded on demand.
+
+INFO. This is an all-or-nothing operation, Rails does not attempt to reload only
+what changed since dependencies between classes makes that really tricky.
+Instead, everything is wiped.
+
+Common Gotchas
+--------------
+
+### Nesting and Qualified Constants
+
+Let's consider
+
+```ruby
+module Admin
+ class UsersController < ApplicationController
+ def index
+ @users = User.all
+ end
+ end
+end
+```
+
+and
+
+```ruby
+class Admin::UsersController < ApplicationController
+ def index
+ @users = User.all
+ end
+end
+```
+
+To resolve `User` Ruby checks `Admin` in the former case, but it does not in
+the latter because it does not belong to the nesting (see [Nesting](#nesting)
+and [Resolution Algorithms](#resolution-algorithms)).
+
+Unfortunately Rails autoloading does not know the nesting in the spot where the
+constant was missing and so it is not able to act as Ruby would. In particular,
+`Admin::User` will get autoloaded in either case.
+
+Albeit qualified constants with `class` and `module` keywords may technically
+work with autoloading in some cases, it is preferable to use relative constants
+instead:
+
+```ruby
+module Admin
+ class UsersController < ApplicationController
+ def index
+ @users = User.all
+ end
+ end
+end
+```
+
+### Autoloading and STI
+
+Single Table Inheritance (STI) is a feature of Active Record that enables
+storing a hierarchy of models in one single table. The API of such models is
+aware of the hierarchy and encapsulates some common needs. For example, given
+these classes:
+
+```ruby
+# app/models/polygon.rb
+class Polygon < ApplicationRecord
+end
+
+# app/models/triangle.rb
+class Triangle < Polygon
+end
+
+# app/models/rectangle.rb
+class Rectangle < Polygon
+end
+```
+
+`Triangle.create` creates a row that represents a triangle, and
+`Rectangle.create` creates a row that represents a rectangle. If `id` is the
+ID of an existing record, `Polygon.find(id)` returns an object of the correct
+type.
+
+Methods that operate on collections are also aware of the hierarchy. For
+example, `Polygon.all` returns all the records of the table, because all
+rectangles and triangles are polygons. Active Record takes care of returning
+instances of their corresponding class in the result set.
+
+Types are autoloaded as needed. For example, if `Polygon.first` is a rectangle
+and `Rectangle` has not yet been loaded, Active Record autoloads it and the
+record is correctly instantiated.
+
+All good, but if instead of performing queries based on the root class we need
+to work on some subclass, things get interesting.
+
+While working with `Polygon` you do not need to be aware of all its descendants,
+because anything in the table is by definition a polygon, but when working with
+subclasses Active Record needs to be able to enumerate the types it is looking
+for. Let's see an example.
+
+`Rectangle.all` only loads rectangles by adding a type constraint to the query:
+
+```sql
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle")
+```
+
+Let's introduce now a subclass of `Rectangle`:
+
+```ruby
+# app/models/square.rb
+class Square < Rectangle
+end
+```
+
+`Rectangle.all` should now return rectangles **and** squares:
+
+```sql
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle", "Square")
+```
+
+But there's a caveat here: How does Active Record know that the class `Square`
+exists at all?
+
+Even if the file `app/models/square.rb` exists and defines the `Square` class,
+if no code yet used that class, `Rectangle.all` issues the query
+
+```sql
+SELECT "polygons".* FROM "polygons"
+WHERE "polygons"."type" IN ("Rectangle")
+```
+
+That is not a bug, the query includes all *known* descendants of `Rectangle`.
+
+A way to ensure this works correctly regardless of the order of execution is to
+manually load the direct subclasses at the bottom of the file that defines each
+intermediate class:
+
+```ruby
+# app/models/rectangle.rb
+class Rectangle < Polygon
+end
+require_dependency 'square'
+```
+
+This needs to happen for every intermediate (non-root and non-leaf) class. The
+root class does not scope the query by type, and therefore does not necessarily
+have to know all its descendants.
+
+### Autoloading and `require`
+
+Files defining constants to be autoloaded should never be `require`d:
+
+```ruby
+require 'user' # DO NOT DO THIS
+
+class UsersController < ApplicationController
+ ...
+end
+```
+
+There are two possible gotchas here in development mode:
+
+1. If `User` is autoloaded before reaching the `require`, `app/models/user.rb`
+runs again because `load` does not update `$LOADED_FEATURES`.
+
+2. If the `require` runs first Rails does not mark `User` as an autoloaded
+constant and changes to `app/models/user.rb` aren't reloaded.
+
+Just follow the flow and use constant autoloading always, never mix
+autoloading and `require`. As a last resort, if some file absolutely needs to
+load a certain file use `require_dependency` to play nice with constant
+autoloading. This option is rarely needed in practice, though.
+
+Of course, using `require` in autoloaded files to load ordinary 3rd party
+libraries is fine, and Rails is able to distinguish their constants, they are
+not marked as autoloaded.
+
+### Autoloading and Initializers
+
+Consider this assignment in `config/initializers/set_auth_service.rb`:
+
+```ruby
+AUTH_SERVICE = if Rails.env.production?
+ RealAuthService
+else
+ MockedAuthService
+end
+```
+
+The purpose of this setup would be that the application uses the class that
+corresponds to the environment via `AUTH_SERVICE`. In development mode
+`MockedAuthService` gets autoloaded when the initializer runs. Let's suppose
+we do some requests, change its implementation, and hit the application again.
+To our surprise the changes are not reflected. Why?
+
+As [we saw earlier](#constant-reloading), Rails removes autoloaded constants,
+but `AUTH_SERVICE` stores the original class object. Stale, non-reachable
+using the original constant, but perfectly functional.
+
+The following code summarizes the situation:
+
+```ruby
+class C
+ def quack
+ 'quack!'
+ end
+end
+
+X = C
+Object.instance_eval { remove_const(:C) }
+X.new.quack # => quack!
+X.name # => C
+C # => uninitialized constant C (NameError)
+```
+
+Because of that, it is not a good idea to autoload constants on application
+initialization.
+
+In the case above we could implement a dynamic access point:
+
+```ruby
+# app/models/auth_service.rb
+class AuthService
+ if Rails.env.production?
+ def self.instance
+ RealAuthService
+ end
+ else
+ def self.instance
+ MockedAuthService
+ end
+ end
+end
+```
+
+and have the application use `AuthService.instance` instead. `AuthService`
+would be loaded on demand and be autoload-friendly.
+
+### `require_dependency` and Initializers
+
+As we saw before, `require_dependency` loads files in an autoloading-friendly
+way. Normally, though, such a call does not make sense in an initializer.
+
+One could think about doing some [`require_dependency`](#require-dependency)
+calls in an initializer to make sure certain constants are loaded upfront, for
+example as an attempt to address the [gotcha with STIs](#autoloading-and-sti).
+
+Problem is, in development mode [autoloaded constants are wiped](#constant-reloading)
+if there is any relevant change in the file system. If that happens then
+we are in the very same situation the initializer wanted to avoid!
+
+Calls to `require_dependency` have to be strategically written in autoloaded
+spots.
+
+### When Constants aren't Missed
+
+#### Relative References
+
+Let's consider a flight simulator. The application has a default flight model
+
+```ruby
+# app/models/flight_model.rb
+class FlightModel
+end
+```
+
+that can be overridden by each airplane, for instance
+
+```ruby
+# app/models/bell_x1/flight_model.rb
+module BellX1
+ class FlightModel < FlightModel
+ end
+end
+
+# app/models/bell_x1/aircraft.rb
+module BellX1
+ class Aircraft
+ def initialize
+ @flight_model = FlightModel.new
+ end
+ end
+end
+```
+
+The initializer wants to create a `BellX1::FlightModel` and nesting has
+`BellX1`, that looks good. But if the default flight model is loaded and the
+one for the Bell-X1 is not, the interpreter is able to resolve the top-level
+`FlightModel` and autoloading is thus not triggered for `BellX1::FlightModel`.
+
+That code depends on the execution path.
+
+These kind of ambiguities can often be resolved using qualified constants:
+
+```ruby
+module BellX1
+ class Plane
+ def flight_model
+ @flight_model ||= BellX1::FlightModel.new
+ end
+ end
+end
+```
+
+Also, `require_dependency` is a solution:
+
+```ruby
+require_dependency 'bell_x1/flight_model'
+
+module BellX1
+ class Plane
+ def flight_model
+ @flight_model ||= FlightModel.new
+ end
+ end
+end
+```
+
+#### Qualified References
+
+WARNING. This gotcha is only possible in Ruby < 2.5.
+
+Given
+
+```ruby
+# app/models/hotel.rb
+class Hotel
+end
+
+# app/models/image.rb
+class Image
+end
+
+# app/models/hotel/image.rb
+class Hotel
+ class Image < Image
+ end
+end
+```
+
+the expression `Hotel::Image` is ambiguous because it depends on the execution
+path.
+
+As [we saw before](#resolution-algorithm-for-qualified-constants), Ruby looks
+up the constant in `Hotel` and its ancestors. If `app/models/image.rb` has
+been loaded but `app/models/hotel/image.rb` hasn't, Ruby does not find `Image`
+in `Hotel`, but it does in `Object`:
+
+```
+$ rails r 'Image; p Hotel::Image' 2>/dev/null
+Image # NOT Hotel::Image!
+```
+
+The code evaluating `Hotel::Image` needs to make sure
+`app/models/hotel/image.rb` has been loaded, possibly with
+`require_dependency`.
+
+In these cases the interpreter issues a warning though:
+
+```
+warning: toplevel constant Image referenced by Hotel::Image
+```
+
+This surprising constant resolution can be observed with any qualifying class:
+
+```
+2.1.5 :001 > String::Array
+(irb):1: warning: toplevel constant Array referenced by String::Array
+ => Array
+```
+
+WARNING. To find this gotcha the qualifying namespace has to be a class,
+`Object` is not an ancestor of modules.
+
+### Autoloading within Singleton Classes
+
+Let's suppose we have these class definitions:
+
+```ruby
+# app/models/hotel/services.rb
+module Hotel
+ class Services
+ end
+end
+
+# app/models/hotel/geo_location.rb
+module Hotel
+ class GeoLocation
+ class << self
+ Services
+ end
+ end
+end
+```
+
+If `Hotel::Services` is known by the time `app/models/hotel/geo_location.rb`
+is being loaded, `Services` is resolved by Ruby because `Hotel` belongs to the
+nesting when the singleton class of `Hotel::GeoLocation` is opened.
+
+But if `Hotel::Services` is not known, Rails is not able to autoload it, the
+application raises `NameError`.
+
+The reason is that autoloading is triggered for the singleton class, which is
+anonymous, and as [we saw before](#generic-procedure), Rails only checks the
+top-level namespace in that edge case.
+
+An easy solution to this caveat is to qualify the constant:
+
+```ruby
+module Hotel
+ class GeoLocation
+ class << self
+ Hotel::Services
+ end
+ end
+end
+```
+
+### Autoloading in `BasicObject`
+
+Direct descendants of `BasicObject` do not have `Object` among their ancestors
+and cannot resolve top-level constants:
+
+```ruby
+class C < BasicObject
+ String # NameError: uninitialized constant C::String
+end
+```
+
+When autoloading is involved that plot has a twist. Let's consider:
+
+```ruby
+class C < BasicObject
+ def user
+ User # WRONG
+ end
+end
+```
+
+Since Rails checks the top-level namespace `User` gets autoloaded just fine the
+first time the `user` method is invoked. You only get the exception if the
+`User` constant is known at that point, in particular in a *second* call to
+`user`:
+
+```ruby
+c = C.new
+c.user # surprisingly fine, User
+c.user # NameError: uninitialized constant C::User
+```
+
+because it detects that a parent namespace already has the constant (see [Qualified
+References](#autoloading-algorithms-qualified-references)).
+
+As with pure Ruby, within the body of a direct descendant of `BasicObject` use
+always absolute constant paths:
+
+```ruby
+class C < BasicObject
+ ::String # RIGHT
+
+ def user
+ ::User # RIGHT
+ end
+end
+```
+
+### Autoloading in the Test Environment
+
+When configuring the `test` environment for autoloading you might consider multiple factors.
+
+For example it might be worth running your tests with an identical setup to production (`config.eager_load = true`, `config.cache_classes = true`) in order to catch any problems before they hit production (this is compensation for the lack of dev-prod parity). However this will slow down the boot time for individual tests on a dev machine (and is not immediately compatible with spring see below). So one possibility is to do this on a
+[CI](https://en.wikipedia.org/wiki/Continuous_integration) machine only (which should run without spring).
+
+On a development machine you can then have your tests running with whatever is fastest (ideally `config.eager_load = false`).
+
+With the [Spring](https://github.com/rails/spring) pre-loader (included with new Rails apps), you ideally keep `config.eager_load = false` as per development. Sometimes you may end up with a hybrid configuration (`config.eager_load = true`, `config.cache_classes = true` AND `config.enable_dependency_loading = true`), see [spring issue](https://github.com/rails/spring/issues/519#issuecomment-348324369). However it might be simpler to keep the same configuration as development, and work out whatever it is that is causing autoloading to fail (perhaps by the results of your CI test results).
+
+Occasionally you may need to explicitly eager_load by using `Rails
+.application.eager_load!` in the setup of your tests -- this might occur if your [tests involve multithreading](https://stackoverflow.com/questions/25796409/in-rails-how-can-i-eager-load-all-code-before-a-specific-rspec-test).
+
+## Troubleshooting
+
+### Tracing Autoloads
+
+Active Support is able to report constants as they are autoloaded. To enable these traces in a Rails application, put the following two lines in some initializer:
+
+```ruby
+ActiveSupport::Dependencies.logger = Rails.logger
+ActiveSupport::Dependencies.verbose = true
+```
+
+### Where is a Given Autoload Triggered?
+
+If constant `Foo` is being autoloaded, and you'd like to know where is that autoload coming from, just throw
+
+```ruby
+puts caller
+```
+
+at the top of `foo.rb` and inspect the printed stack trace.
+
+### Which Constants Have Been Autoloaded?
+
+At any given time,
+
+```ruby
+ActiveSupport::Dependencies.autoloaded_constants
+```
+
+has the collection of constants that have been autoloaded so far.
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 4681574edd..4acc25bdc2 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -368,7 +368,7 @@ irb(main):001:0>
Inside the `rails console` you have access to the `app` and `helper` instances.
-With the `app` method you can access URL and path helpers, as well as do requests.
+With the `app` method you can access named route helpers, as well as do requests.
```bash
>> app.root_path
@@ -601,6 +601,8 @@ The `tmp:` namespaced commands will help you clear and create the `Rails.root/tm
### Miscellaneous
+* `rails initializers` prints out all defined initializers in the order they are invoked by Rails.
+* `rails middleware` lists Rack middleware stack enabled for your app.
* `rails stats` is great for looking at statistics on your code, displaying things like KLOCs (thousands of lines of code) and your code to test ratio.
* `rails secret` will give you a pseudo-random key to use for your session secret.
* `rails time:zones:all` lists all the timezones Rails knows about.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index ffff9417b9..c5d3d09bd0 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -353,7 +353,7 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is `true` by default.
-* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:nsec`.
+* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:usec`.
* `config.active_record.record_timestamps` is a boolean value which controls whether or not timestamping of `create` and `update` operations on a model occur. The default value is `true`.
@@ -844,6 +844,8 @@ You can find more detailed configuration options in the
* `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 application/mathml+xml text/cache-manifest)`.
+* `config.active_storage.content_types_allowed_inline` accepts an array of strings indicating the content types that Active Storage allows to serve as inline. The default is `%w(image/png image/gif image/jpg image/jpeg image/vnd.adobe.photoshop image/vnd.microsoft.icon application/pdf)`.
+
* `config.active_storage.queues.analysis` accepts a symbol indicating the Active Job queue to use for analysis jobs. When this option is `nil`, analysis jobs are sent to the default Active Job queue (see `config.active_job.default_queue_name`).
```ruby
@@ -881,7 +883,11 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
config.active_storage.routes_prefix = '/files'
```
- The default is `/rails/active_storage`
+ The default is `/rails/active_storage`.
+
+* `config.active_storage.replace_on_assign_to_many` determines whether assigning to a collection of attachments declared with `has_many_attached` replaces any existing attachments or appends to them. The default is `true`.
+
+* `config.active_storage.draw_routes` can be used to toggle Active Storage route generation. The default is `true`.
### Results of `load_defaults`
@@ -917,6 +923,7 @@ text/javascript image/svg+xml application/postscript application/x-shockwave-fla
- `config.active_job.return_false_on_aborted_enqueue`: `true`
- `config.active_storage.queues.analysis`: `:active_storage_analysis`
- `config.active_storage.queues.purge`: `:active_storage_purge`
+- `config.active_storage.replace_on_assign_to_many`: `true`
- `config.active_record.collection_cache_versioning`: `true`
### Configuring a Database
@@ -1541,7 +1548,7 @@ Disallow: /
```
To block just specific pages, it's necessary to use a more complex syntax. Learn
-it on the [official documentation](http://www.robotstxt.org/robotstxt.html).
+it on the [official documentation](https://www.robotstxt.org/robotstxt.html).
Evented File System Monitor
---------------------------
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index d3706a4dbf..9c28ff6a9c 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -139,7 +139,7 @@ changes to the master branch.
When working with documentation, please take into account the [API Documentation Guidelines](api_documentation_guidelines.html) and the [Ruby on Rails Guides Guidelines](ruby_on_rails_guides_guidelines.html).
-NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
+NOTE: For documentation changes, your commit message should include [ci skip]. This will skip running the test suite, helping us to cut down on our server costs. Keep in mind that you should only skip CI when your change touches documentation exclusively.
Translating Rails Guides
------------------------
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 8df1c601a7..da11236064 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -139,9 +139,13 @@
url: initialization.html
description: This guide explains the internals of the Rails initialization process.
-
- name: Autoloading and Reloading Constants
+ name: Autoloading and Reloading Constants (Zeitwerk Mode)
url: autoloading_and_reloading_constants.html
- description: This guide documents how autoloading and reloading constants work.
+ description: This guide documents how autoloading and reloading constants work (Zeitwerk mode).
+ -
+ name: Autoloading and Reloading Constants (Classic Mode)
+ url: autoloading_and_reloading_constants_classic_mode.html
+ description: This guide documents how autoloading and reloading constants work (Classic mode).
-
name: "Caching with Rails: An Overview"
url: caching_with_rails.html
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 90b08b00f0..b3ac243af9 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -220,7 +220,7 @@ Inside the `app` directory are the standard `assets`, `controllers`, `helpers`,
`jobs`, `mailers`, `models`, and `views` directories that you should be familiar with
from an application. We'll look more into models in a future section, when we're writing the engine.
-Within the `app/assets` directory, there are the `images`, `javascripts` and
+Within the `app/assets` directory, there are the `images` and
`stylesheets` directories which, again, you should be familiar with due to their
similarity to an application. One difference here, however, is that each
directory contains a sub-directory with the engine name. Because this engine is
@@ -264,7 +264,7 @@ contains a file called `application_helper.rb`. This file will provide any
common functionality for the helpers of the engine. The `blorgh` directory
is where the other helpers for the engine will go. By placing them within
this namespaced directory, you prevent them from possibly clashing with
-identically-named helpers within other engines or even within the
+identically-named route helpers within other engines or even within the
application.
Within the `app/jobs` directory there is a `blorgh` directory that
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index 6418005921..bee6755b52 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -307,7 +307,7 @@ When dealing with RESTful resources, calls to `form_with` can get significantly
## Creating a new article
# long-style:
form_with(model: @article, url: articles_path)
-short-style:
+# short-style:
form_with(model: @article)
## Editing an existing article
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 64f4a3b6b3..ce45dbb2a7 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -28,7 +28,7 @@ curve diving straight into Rails. There are several curated lists of online reso
for learning Ruby:
* [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/)
-* [List of Free Programming Books](https://github.com/vhf/free-programming-books/blob/master/free-programming-books.md#ruby)
+* [List of Free Programming Books](https://github.com/EbookFoundation/free-programming-books/blob/master/free-programming-books.md#ruby)
Be aware that some resources, while still excellent, cover versions of Ruby as old as
1.6, and commonly 1.8, and will not include some syntax that you will see in day-to-day
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index c19b2cffc5..556c85cc0f 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -108,6 +108,8 @@ A standard Rails application depends on several gems, specifically:
* activerecord
* activestorage
* activesupport
+* actionmailbox
+* actiontext
* arel
* builder
* bundler
@@ -538,6 +540,8 @@ require "rails"
action_mailer/railtie
active_job/railtie
action_cable/engine
+ action_mailbox/engine
+ action_text/engine
rails/test_unit/railtie
sprockets/railtie
).each do |railtie|
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 39935cd2ef..ce90a60e36 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -149,25 +149,6 @@ Rails knows that this view belongs to a different controller because of the embe
render template: "products/show"
```
-#### Rendering an Arbitrary File
-
-The `render` method can also use a view that's entirely outside of your application:
-
-```ruby
-render file: "/u/apps/warehouse_app/current/app/views/products/show"
-```
-
-The `:file` option takes an absolute file-system path. Of course, you need to have rights
-to the view that you're using to render the content.
-
-NOTE: Using the `:file` option in combination with users input can lead to security problems
-since an attacker could use this action to access security sensitive files in your file system.
-
-NOTE: By default, the file is rendered using the current layout.
-
-TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to
-render a file, because Windows filenames do not have the same format as Unix filenames.
-
#### Wrapping it up
The above three ways of rendering (rendering another template within the controller, rendering a template within another controller, and rendering an arbitrary file on the file system) are actually variants of the same action.
@@ -178,17 +159,9 @@ In fact, in the BooksController class, inside of the update action where we want
render :edit
render action: :edit
render "edit"
-render "edit.html.erb"
render action: "edit"
-render action: "edit.html.erb"
render "books/edit"
-render "books/edit.html.erb"
render template: "books/edit"
-render template: "books/edit.html.erb"
-render "/path/to/rails/app/views/books/edit"
-render "/path/to/rails/app/views/books/edit.html.erb"
-render file: "/path/to/rails/app/views/books/edit"
-render file: "/path/to/rails/app/views/books/edit.html.erb"
```
Which one you use is really a matter of style and convention, but the rule of thumb is to use the simplest one that makes sense for the code you are writing.
@@ -287,6 +260,23 @@ time.
NOTE: Unless overridden, your response returned from this render option will be
`text/plain`, as that is the default content type of Action Dispatch response.
+#### Rendering raw file
+
+Rails can render a raw file from an absolute path. This is useful for
+conditionally rendering static files like error pages.
+
+```ruby
+render file: "#{Rails.root}/public/404.html", layout: false
+```
+
+This renders the raw file (it doesn't support ERB or other handlers). By
+default it is rendered within the current layout.
+
+WARNING: Using the `:file` option in combination with users input can lead to security problems
+since an attacker could use this action to access security sensitive files in your file system.
+
+TIP: `send_file` is often a faster and better option if a layout isn't required.
+
#### Options for `render`
Calls to the `render` method generally accept five options:
@@ -303,7 +293,7 @@ Calls to the `render` method generally accept five options:
By default, Rails will serve the results of a rendering operation with the MIME content-type of `text/html` (or `application/json` if you use the `:json` option, or `application/xml` for the `:xml` option.). There are times when you might like to change this, and you can do so by setting the `:content_type` option:
```ruby
-render file: filename, content_type: "application/rss"
+render template: "feed", content_type: "application/rss"
```
##### The `:layout` Option
diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md
index 982df26987..e0e79fc41b 100644
--- a/guides/source/rails_application_templates.md
+++ b/guides/source/rails_application_templates.md
@@ -82,10 +82,10 @@ end
Adds the given source to the generated application's `Gemfile`.
-For example, if you need to source a gem from `"http://code.whytheluckystiff.net"`:
+For example, if you need to source a gem from `"http://gems.github.com"`:
```ruby
-add_source "http://code.whytheluckystiff.net"
+add_source "http://gems.github.com"
```
If block is given, gem entries in block are wrapped into the source group.
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 4aeb9ee585..161984c993 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -210,7 +210,7 @@ end
This will create a number of routes for each of the `articles` and `comments` controller. For `Admin::ArticlesController`, Rails will create:
-| HTTP Verb | Path | Controller#Action | Named Helper |
+| HTTP Verb | Path | Controller#Action | Named Route Helper |
| --------- | ------------------------ | ---------------------- | ---------------------------- |
| GET | /admin/articles | admin/articles#index | admin_articles_path |
| GET | /admin/articles/new | admin/articles#new | new_admin_article_path |
@@ -250,7 +250,7 @@ resources :articles, path: '/admin/articles'
In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `ArticlesController`:
-| HTTP Verb | Path | Controller#Action | Named Helper |
+| HTTP Verb | Path | Controller#Action | Named Route Helper |
| --------- | ------------------------ | -------------------- | ---------------------- |
| GET | /admin/articles | articles#index | articles_path |
| GET | /admin/articles/new | articles#new | new_article_path |
@@ -373,7 +373,7 @@ end
The comments resource here will have the following routes generated for it:
-| HTTP Verb | Path | Controller#Action | Named Helper |
+| HTTP Verb | Path | Controller#Action | Named Route Helper |
| --------- | -------------------------------------------- | ----------------- | ------------------------ |
| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path |
| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path |
@@ -383,7 +383,7 @@ The comments resource here will have the following routes generated for it:
| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path |
| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path |
-The `:shallow_prefix` option adds the specified parameter to the named helpers:
+The `:shallow_prefix` option adds the specified parameter to the named route helpers:
```ruby
scope shallow_prefix: "sekret" do
@@ -395,7 +395,7 @@ end
The comments resource here will have the following routes generated for it:
-| HTTP Verb | Path | Controller#Action | Named Helper |
+| HTTP Verb | Path | Controller#Action | Named Route Helper |
| --------- | -------------------------------------------- | ----------------- | --------------------------- |
| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path |
| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path |
@@ -638,7 +638,7 @@ You can specify a name for any route using the `:as` option:
get 'exit', to: 'sessions#destroy', as: :logout
```
-This will create `logout_path` and `logout_url` as named helpers in your application. Calling `logout_path` will return `/exit`
+This will create `logout_path` and `logout_url` as named route helpers in your application. Calling `logout_path` will return `/exit`
You can also use this to override routing methods defined by resources, like this:
@@ -934,7 +934,7 @@ resources :photos, controller: 'images'
will recognize incoming paths beginning with `/photos` but route to the `Images` controller:
-| HTTP Verb | Path | Controller#Action | Named Helper |
+| HTTP Verb | Path | Controller#Action | Named Route Helper |
| --------- | ---------------- | ----------------- | -------------------- |
| GET | /photos | images#index | photos_path |
| GET | /photos/new | images#new | new_photo_path |
@@ -982,7 +982,7 @@ NOTE: Of course, you can use the more advanced constraints available in non-reso
TIP: By default the `:id` parameter doesn't accept dots - this is because the dot is used as a separator for formatted routes. If you need to use a dot within an `:id` add a constraint which overrides this - for example `id: /[^\/]+/` allows anything except a slash.
-### Overriding the Named Helpers
+### Overriding the Named Route Helpers
The `:as` option lets you override the normal naming for the named route helpers. For example:
@@ -992,7 +992,7 @@ resources :photos, as: 'images'
will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the `:as` option to name the helpers.
-| HTTP Verb | Path | Controller#Action | Named Helper |
+| HTTP Verb | Path | Controller#Action | Named Route Helper |
| --------- | ---------------- | ----------------- | -------------------- |
| GET | /photos | photos#index | images_path |
| GET | /photos/new | photos#new | new_image_path |
@@ -1097,7 +1097,7 @@ end
Rails now creates routes to the `CategoriesController`.
-| HTTP Verb | Path | Controller#Action | Named Helper |
+| HTTP Verb | Path | Controller#Action | Named Route Helper |
| --------- | -------------------------- | ------------------ | ----------------------- |
| GET | /kategorien | categories#index | categories_path |
| GET | /kategorien/neu | categories#new | new_category_path |
diff --git a/guides/source/security.md b/guides/source/security.md
index 22c122d4b9..5bb7a51524 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -1211,4 +1211,4 @@ The security landscape shifts and it is important to keep up to date, because mi
* Subscribe to the Rails security [mailing list](https://groups.google.com/forum/#!forum/rubyonrails-security).
* [Brakeman - Rails Security Scanner](https://brakemanscanner.org/) - To perform static security analysis for Rails applications.
* [Keep up to date on the other application layers](http://secunia.com/) (they have a weekly newsletter, too).
-* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://www.owasp.org/index.php/DOM_based_XSS_Prevention_Cheat_Sheet).
+* A [good security blog](https://www.owasp.org) including the [Cross-Site scripting Cheat Sheet](https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md).
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 54f014293d..39a291ed97 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -78,6 +78,20 @@ Upgrading from Rails 5.2 to Rails 6.0
For more information on changes made to Rails 6.0 please see the [release notes](6_0_release_notes.html).
+### Using Webpacker
+
+[Webpacker](https://github.com/rails/webpacker)
+is the default JavaScript compiler for Rails 6. But if you are upgrading the app, it is not activated by default.
+If you want to use Webpacker, then include it in your Gemfile and install it:
+
+```ruby
+gem "webpacker"
+```
+
+```sh
+bin/rails webpacker:install
+```
+
### Force SSL
The `force_ssl` method on controllers has been deprecated and will be removed in
@@ -184,41 +198,21 @@ That may be handy if you need to preload STIs or configure a custom inflector, f
If the application being upgraded autoloads correctly, the project structure should be already mostly compatible.
-However, `classic` mode infers file names from missing constant names (`underscore`), whereas `zeitwerk` mode infers constant names from file names (`camelize`). These helpers are not always inverse of each other, in particular if acronyms are involved. For instance, `"FOO".underscore` is `"foo"`, but `"foo".camelize` is `"Foo"`, not `"FOO"`. Compatibility can be checked by setting `classic` mode first temporarily:
+However, `classic` mode infers file names from missing constant names (`underscore`), whereas `zeitwerk` mode infers constant names from file names (`camelize`). These helpers are not always inverse of each other, in particular if acronyms are involved. For instance, `"FOO".underscore` is `"foo"`, but `"foo".camelize` is `"Foo"`, not `"FOO"`.
-```ruby
-# config/application.rb
+Compatibility can be checked with the `zeitwerk:check` task:
-config.load_defaults "6.0"
-config.autoloader = :classic
```
-
-and then running
-
+$ bin/rails zeitwerk:check
+Hold on, I am eager loading the application.
+All is good!
```
-bin/rails zeitwerk:check
-```
-
-When all is good, you can delete `config.autoloader = :classic`.
#### require_dependency
All known use cases of `require_dependency` have been eliminated, you should grep the project and delete them.
-In the case of STIs with a hierarchy of more than two levels, you can preload the leaves of the hierarchy in an initializer:
-
-```ruby
-# config/initializers/preload_stis.rb
-
-# By preloading leaves, the hierarchy is loaded upwards following
-# the references to superclasses in the class definitions.
-sti_leaves = %w(
- app/models/leaf1.rb
- app/models/leaf2.rb
- app/models/leaf3.rb
-)
-Rails.autoloaders.main.preload(sti_leaves)
-```
+If your application has STIs, please check their section in the guide [Autoloading and Reloading Constants (Zeitwerk Mode)](autoloading_and_reloading_constants.html#single-table-inheritance).
#### Qualified names in class and module definitions
@@ -270,6 +264,16 @@ In that case, `app/models/concerns` is assumed to be a root directory (because i
The `Concerns::` namespace worked with the classic autoloader as a side-effect of the implementation, but it was not really an intended behavior. An application using `Concerns::` needs to rename those classes and modules to be able to run in `zeitwerk` mode.
+#### Having `app` in the autoload paths
+
+Some projects want something like `app/api/base.rb` to define `API::Base`, and add `app` to the autoload paths to accomplish that in `classic` mode. Since Rails adds all subdirectories of `app` to the autoload paths automatically, we have another situation in which there are nested root directories, so that setup no longer works. Similar principle we explained above with `concerns`.
+
+If you want to keep that structure, you'll need to delete the subdirectory from the autoload paths in an initializer:
+
+```ruby
+ActiveSupport::Dependencies.autoload_paths.delete("#{Rails.root}/app/api")
+```
+
#### Autoloaded Constants and Explicit Namespaces
If a namespace is defined in a file, as `Hotel` is here:
@@ -391,6 +395,12 @@ To fix this, just remove the wildcards:
config.autoload_paths << "#{config.root}/lib"
```
+#### Eager loading and autoloading are consistent
+
+In `classic` mode, if `app/models/foo.rb` defines `Bar`, you won't be able to autoload that file, but eager loading will work because it loads files recursively blindly. This can be a source of errors if you test things first eager loading, execution may fail later autoloading.
+
+In `zeitwerk` mode both loading modes are consistent, they fail and err in the same files.
+
#### How to Use the Classic Autoloader in Rails 6
Applications can load Rails 6 defaults and still use the classic autoloader by setting `config.autoloader` this way:
@@ -402,6 +412,54 @@ config.load_defaults "6.0"
config.autoloader = :classic
```
+### Active Storage assignment behavior change
+
+In Rails 5.2, assigning to a collection of attachments declared with `has_many_attached` appended new files:
+
+```ruby
+class User < ApplicationRecord
+ has_many_attached :highlights
+end
+
+user.highlights.attach(filename: "funky.jpg", ...)
+user.higlights.count # => 1
+
+blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg", ...)
+user.update!(highlights: [ blob ])
+
+user.highlights.count # => 2
+user.highlights.first.filename # => "funky.jpg"
+user.highlights.second.filename # => "town.jpg"
+```
+
+With the default configuration for Rails 6.0, assigning to a collection of attachments replaces existing files
+instead of appending to them. This matches Active Record behavior when assigning to a collection association:
+
+```ruby
+user.highlights.attach(filename: "funky.jpg", ...)
+user.highlights.count # => 1
+
+blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg", ...)
+user.update!(highlights: [ blob ])
+
+user.highlights.count # => 1
+user.highlights.first.filename # => "town.jpg"
+```
+
+`#attach` can be used to add new attachments without removing the existing ones:
+
+```ruby
+blob = ActiveStorage::Blob.create_after_upload!(filename: "town.jpg", ...)
+user.highlights.attach(blob)
+
+user.highlights.count # => 2
+user.highlights.first.filename # => "funky.jpg"
+user.highlights.second.filename # => "town.jpg"
+```
+
+Opt in to the new default behavior by setting `config.active_storage.replace_on_assign_to_many` to `true`.
+The old behavior will be deprecated in Rails 6.1 and removed in a subsequent release.
+
Upgrading from Rails 5.1 to Rails 5.2
-------------------------------------
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index 8cf8efefd0..28af1cd88d 100644
--- a/guides/source/working_with_javascript_in_rails.md
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -14,6 +14,7 @@ After reading this guide, you will know:
* How Rails' built-in helpers assist you.
* How to handle Ajax on the server side.
* The Turbolinks gem.
+* How to include your Cross-Site Request Forgery token in request headers
-------------------------------------------------------------------------------
@@ -524,6 +525,23 @@ For more details, including other events you can bind to, check out [the
Turbolinks
README](https://github.com/turbolinks/turbolinks/blob/master/README.md).
+Cross-Site Request Forgery (CSRF) token in Ajax
+----
+
+When using another library to make Ajax calls, it is necessary to add
+the security token as a default header for Ajax calls in your library. To get
+the token:
+
+```javascript
+var token = document.getElementsByName('csrf-token')[0].content
+```
+
+You can then submit this token as a `X-CSRF-Token` header for your
+Ajax request. You do not need to add a CSRF token for GET requests,
+only non-GET ones.
+
+You can read more about about Cross-Site Request Forgery in [Security](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf)
+
Other Resources
---------------
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index 1f533a8c04..440d2953c3 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -5,7 +5,6 @@ require "rails/ruby_version_check"
require "pathname"
require "active_support"
-require "active_support/dependencies/autoload"
require "active_support/core_ext/kernel/reporting"
require "active_support/core_ext/module/delegation"
require "active_support/core_ext/array/extract_options"
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index dd1770f0ea..cbaab6cc33 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -270,7 +270,9 @@ module Rails
"action_dispatch.use_cookies_with_metadata" => config.action_dispatch.use_cookies_with_metadata,
"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_nonce_generator" => config.content_security_policy_nonce_generator
+ "action_dispatch.content_security_policy_nonce_generator" => config.content_security_policy_nonce_generator,
+ "action_dispatch.content_security_policy_nonce_directives" => config.content_security_policy_nonce_directives,
+ "action_dispatch.feature_policy" => config.feature_policy,
)
end
end
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 72c7ff169f..934578e9f1 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -18,8 +18,8 @@ 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,
- :content_security_policy_nonce_generator, :require_master_key, :credentials,
- :disable_sandbox, :add_autoload_paths_to_load_path
+ :content_security_policy_nonce_generator, :content_security_policy_nonce_directives,
+ :require_master_key, :credentials, :disable_sandbox, :add_autoload_paths_to_load_path
attr_reader :encoding, :api_only, :loaded_config_version, :autoloader
@@ -60,6 +60,7 @@ module Rails
@content_security_policy = nil
@content_security_policy_report_only = false
@content_security_policy_nonce_generator = nil
+ @content_security_policy_nonce_directives = nil
@require_master_key = false
@loaded_config_version = nil
@credentials = ActiveSupport::OrderedOptions.new
@@ -68,6 +69,7 @@ module Rails
@autoloader = :classic
@disable_sandbox = false
@add_autoload_paths_to_load_path = true
+ @feature_policy = nil
end
def load_defaults(target_version)
@@ -143,6 +145,8 @@ module Rails
if respond_to?(:active_storage)
active_storage.queues.analysis = :active_storage_analysis
active_storage.queues.purge = :active_storage_purge
+
+ active_storage.replace_on_assign_to_many = true
end
if respond_to?(:active_record)
@@ -208,7 +212,7 @@ module Rails
yaml = Pathname.new(path)
erb = DummyERB.new(yaml.read)
- YAML.load(erb.result)
+ YAML.load(erb.result) || {}
else
{}
end
@@ -300,6 +304,14 @@ module Rails
end
end
+ def feature_policy(&block)
+ if block_given?
+ @feature_policy = ActionDispatch::FeaturePolicy.new(&block)
+ else
+ @feature_policy
+ end
+ end
+
def autoloader=(autoloader)
case autoloader
when :classic
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 3659c0ac3a..572f51fca2 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -68,6 +68,7 @@ module Rails
unless config.api_only
middleware.use ::ActionDispatch::ContentSecurityPolicy::Middleware
+ middleware.use ::ActionDispatch::FeaturePolicy::Middleware
end
middleware.use ::Rack::Head
diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb
index 09082282f3..aa5c0d0b5b 100644
--- a/railties/lib/rails/code_statistics.rb
+++ b/railties/lib/rails/code_statistics.rb
@@ -44,7 +44,7 @@ class CodeStatistics #:nodoc:
Dir.foreach(directory) do |file_name|
path = "#{directory}/#{file_name}"
- if File.directory?(path) && (/^\./ !~ file_name)
+ if File.directory?(path) && !(/^\./.match?(file_name))
stats.add(calculate_directory_statistics(path, pattern))
elsif file_name&.match?(pattern)
stats.add_by_file_path(path)
diff --git a/railties/lib/rails/code_statistics_calculator.rb b/railties/lib/rails/code_statistics_calculator.rb
index 85f86bdbd0..8dd415d9d1 100644
--- a/railties/lib/rails/code_statistics_calculator.rb
+++ b/railties/lib/rails/code_statistics_calculator.rb
@@ -58,20 +58,20 @@ class CodeStatisticsCalculator #:nodoc:
@lines += 1
if comment_started
- if patterns[:end_block_comment] && line =~ patterns[:end_block_comment]
+ if patterns[:end_block_comment] && patterns[:end_block_comment].match?(line)
comment_started = false
end
next
else
- if patterns[:begin_block_comment] && line =~ patterns[:begin_block_comment]
+ if patterns[:begin_block_comment] && patterns[:begin_block_comment].match?(line)
comment_started = true
next
end
end
- @classes += 1 if patterns[:class] && line =~ patterns[:class]
- @methods += 1 if patterns[:method] && line =~ patterns[:method]
- if line !~ /^\s*$/ && (patterns[:line_comment].nil? || line !~ patterns[:line_comment])
+ @classes += 1 if patterns[:class] && patterns[:class].match?(line)
+ @methods += 1 if patterns[:method] && patterns[:method].match?(line)
+ if !line.match?(/^\s*$/) && (patterns[:line_comment].nil? || !line.match?(patterns[:line_comment]))
@code_lines += 1
end
end
diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb
index f09aa3ae0d..7e6e968c92 100644
--- a/railties/lib/rails/command.rb
+++ b/railties/lib/rails/command.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support"
-require "active_support/dependencies/autoload"
require "active_support/core_ext/enumerable"
require "active_support/core_ext/object/blank"
diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb
index a22b198c66..415bab199f 100644
--- a/railties/lib/rails/command/base.rb
+++ b/railties/lib/rails/command/base.rb
@@ -52,7 +52,7 @@ module Rails
def inherited(base) #:nodoc:
super
- if base.name && base.name !~ /Base$/
+ if base.name && !base.name.match?(/Base$/)
Rails::Command.subclasses << base
end
end
diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb
index 7fb2a99e67..90650059f4 100644
--- a/railties/lib/rails/command/behavior.rb
+++ b/railties/lib/rails/command/behavior.rb
@@ -44,7 +44,7 @@ module Rails
require path
return
rescue LoadError => e
- raise unless e.message =~ /#{Regexp.escape(path)}$/
+ raise unless /#{Regexp.escape(path)}$/.match?(e.message)
rescue Exception => e
warn "[WARNING] Could not load #{command_type} #{path.inspect}. Error: #{e.message}.\n#{e.backtrace.join("\n")}"
end
diff --git a/railties/lib/rails/command/environment_argument.rb b/railties/lib/rails/command/environment_argument.rb
index 9945fd1430..df3cc1b2bb 100644
--- a/railties/lib/rails/command/environment_argument.rb
+++ b/railties/lib/rails/command/environment_argument.rb
@@ -28,7 +28,7 @@ module Rails
if available_environments.include? env
env
else
- %w( production development test ).detect { |e| e =~ /^#{env}/ } || env
+ %w( production development test ).detect { |e| /^#{env}/.match?(e) } || env
end
end
diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE
index c8d3fb9eda..6b896ab02a 100644
--- a/railties/lib/rails/commands/credentials/USAGE
+++ b/railties/lib/rails/commands/credentials/USAGE
@@ -30,6 +30,21 @@ You could prepend that to your server's start command like this:
RAILS_MASTER_KEY="very-secret-and-secure" server.start
+=== Set up Git to Diff Credentials
+
+Rails provides `rails credentials:diff --enable` to instruct Git to call `rails credentials:diff`
+when `git diff` is run on a credentials file.
+
+Running the command enrolls the project such that all credentials files use the
+"rails_credentials" diff driver in .gitattributes.
+
+Additionally since Git requires the driver itself to be set up in a config file
+that isn't tracked Rails automatically ensures it's configured when running
+`credentials:edit`.
+
+Otherwise each co-worker would have to run enable manually, including on each new
+repo clone.
+
=== Editing Credentials
This will open a temporary file in `$EDITOR` with the decrypted contents to edit
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
index e23a1b3008..9cde44558b 100644
--- a/railties/lib/rails/commands/credentials/credentials_command.rb
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "pathname"
require "active_support"
require "rails/command/helpers/editor"
require "rails/command/environment_argument"
@@ -10,6 +11,9 @@ module Rails
include Helpers::Editor
include EnvironmentArgument
+ require_relative "credentials_command/diffing"
+ include Diffing
+
self.environment_desc = "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key"
no_commands do
@@ -28,6 +32,7 @@ module Rails
ensure_encryption_key_has_been_added if credentials.key.nil?
ensure_credentials_have_been_added
+ ensure_rails_credentials_driver_is_set
catch_editing_exceptions do
change_credentials_in_system_editor
@@ -45,6 +50,23 @@ module Rails
say credentials.read.presence || missing_credentials_message
end
+ option :enroll, type: :boolean, default: false,
+ desc: "Enrolls project in credential file diffing with `git diff`"
+
+ def diff(content_path = nil)
+ if @content_path = content_path
+ extract_environment_option_from_argument(default_environment: extract_environment_from_path(content_path))
+ require_application!
+
+ say credentials.read.presence || credentials.content_path.read
+ else
+ require_application!
+ enroll_project_in_credentials_diffing if options[:enroll]
+ end
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage
+ say credentials.content_path.read
+ end
+
private
def credentials
Rails.application.encrypted(content_path, key_path: key_path)
@@ -79,13 +101,16 @@ module Rails
def content_path
- options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
+ @content_path ||= options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc"
end
def key_path
options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key"
end
+ def extract_environment_from_path(path)
+ available_environments.find { |env| path.include? env } if path.match?(/\.yml\.enc$/)
+ end
def encryption_key_file_generator
require "rails/generators"
diff --git a/railties/lib/rails/commands/credentials/credentials_command/diffing.rb b/railties/lib/rails/commands/credentials/credentials_command/diffing.rb
new file mode 100644
index 0000000000..1d34c68074
--- /dev/null
+++ b/railties/lib/rails/commands/credentials/credentials_command/diffing.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Rails::Command::CredentialsCommand::Diffing # :nodoc:
+ def enroll_project_in_credentials_diffing
+ if enrolled?
+ true
+ else
+ gitattributes.write(<<~end_of_template, mode: "a")
+ config/credentials/*.yml.enc diff=rails_credentials
+ config/credentials.yml.enc diff=rails_credentials
+ end_of_template
+
+ say "Project successfully enrolled!"
+ say "Rails ensures the rails_credentials diff driver is set when running `credentials:edit`. See `credentials:help` for more."
+ end
+ end
+
+ def ensure_rails_credentials_driver_is_set
+ set_driver if enrolled? && !driver_configured?
+ end
+
+ private
+ def enrolled?
+ gitattributes.read.match?(/config\/credentials(\/\*)?\.yml\.enc diff=rails_credentials/)
+ rescue Errno::ENOENT
+ false
+ end
+
+ def driver_configured?
+ system "git config --get diff.rails_credentials.textconv", out: File::NULL
+ end
+
+ def set_driver
+ puts "running"
+ system "git config diff.rails_credentials.textconv 'bin/rails credentials:diff'"
+ end
+
+ def gitattributes
+ Rails.root.join(".gitattributes")
+ end
+end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 46f1d38b96..f8f5ff443a 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -3,6 +3,7 @@
require "rails/railtie"
require "rails/engine/railties"
require "active_support/core_ext/module/delegation"
+require "active_support/core_ext/object/try"
require "pathname"
require "thread"
@@ -362,7 +363,7 @@ module Rails
base.called_from = begin
call_stack = caller_locations.map { |l| l.absolute_path || l.path }
- File.dirname(call_stack.detect { |p| p !~ %r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack] })
+ File.dirname(call_stack.detect { |p| !p.match?(%r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack]) })
end
end
diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb
index 4143b3c881..612bd170c6 100644
--- a/railties/lib/rails/engine/configuration.rb
+++ b/railties/lib/rails/engine/configuration.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "rails/railtie/configuration"
+require "yaml"
module Rails
class Engine
@@ -40,7 +41,7 @@ module Rails
paths.add "app", eager_load: true,
glob: "{*,*/concerns}",
- exclude: %w(assets javascript)
+ exclude: ["assets", webpacker_path]
paths.add "app/assets", glob: "*"
paths.add "app/controllers", eager_load: true
paths.add "app/channels", eager_load: true, glob: "**/*_channel.rb"
@@ -85,6 +86,14 @@ module Rails
def autoload_paths
@autoload_paths ||= paths.autoload_paths
end
+
+ def webpacker_path
+ if File.file?("#{Rails.root}/config/webpacker.yml")
+ YAML.load_file("#{Rails.root}/config/webpacker.yml")[Rails.env]["source_path"]&.gsub("app/", "")
+ else
+ "javascript"
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index 709863275d..aa7ef1077e 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -6,8 +6,6 @@ $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.inc
require "thor/group"
require "rails/command"
-require "active_support"
-require "active_support/core_ext/object/blank"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/hash/deep_merge"
@@ -130,11 +128,6 @@ module Rails
end
end
- # Remove the color from output.
- def no_color!
- Thor::Base.shell = Thor::Shell::Basic
- end
-
# Returns an array of generator namespaces that are hidden.
# Generator namespaces may be hidden for a variety of reasons.
# Some are aliased such as "rails:migration" and can be
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 406a5b8fc7..b6225cd8c0 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -40,8 +40,7 @@ module Rails
in_root do
str = "gem #{parts.join(", ")}"
str = indentation + str
- str = "\n" + str
- append_file "Gemfile", str, verbose: false
+ append_file_with_newline "Gemfile", str, verbose: false
end
end
@@ -58,9 +57,9 @@ module Rails
log :gemfile, "group #{str}"
in_root do
- append_file "Gemfile", "\ngroup #{str} do", force: true
+ append_file_with_newline "Gemfile", "\ngroup #{str} do", force: true
with_indentation(&block)
- append_file "Gemfile", "\nend\n", force: true
+ append_file_with_newline "Gemfile", "end", force: true
end
end
@@ -71,9 +70,13 @@ module Rails
log :github, "github #{str}"
in_root do
- append_file "Gemfile", "\n#{indentation}github #{str} do", force: true
+ if @indentation.zero?
+ append_file_with_newline "Gemfile", "\ngithub #{str} do", force: true
+ else
+ append_file_with_newline "Gemfile", "#{indentation}github #{str} do", force: true
+ end
with_indentation(&block)
- append_file "Gemfile", "\n#{indentation}end", force: true
+ append_file_with_newline "Gemfile", "#{indentation}end", force: true
end
end
@@ -91,9 +94,9 @@ module Rails
in_root do
if block
- append_file "Gemfile", "\nsource #{quote(source)} do", force: true
+ append_file_with_newline "Gemfile", "\nsource #{quote(source)} do", force: true
with_indentation(&block)
- append_file "Gemfile", "\nend\n", force: true
+ append_file_with_newline "Gemfile", "end", force: true
else
prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false
end
@@ -344,6 +347,13 @@ module Rails
ensure
@indentation -= 1
end
+
+ # Append string to a file with a newline if necessary
+ def append_file_with_newline(path, str, options = {})
+ gsub_file path, /\n?\z/, options do |match|
+ match.end_with?("\n") ? "" : "\n#{str}\n"
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index dbfb7337f0..ed0215bda9 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -400,7 +400,7 @@ module Rails
end
def os_supports_listen_out_of_the_box?
- RbConfig::CONFIG["host_os"] =~ /darwin|linux/
+ /darwin|linux/.match?(RbConfig::CONFIG["host_os"])
end
def run_bundle
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 0b91e3223e..1d3c947cb0 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -20,6 +20,8 @@ module Rails
class_option :skip_namespace, type: :boolean, default: false,
desc: "Skip namespace (affects only isolated applications)"
+ class_option :skip_collision_check, type: :boolean, default: false,
+ desc: "Skip collision check"
add_runtime_options!
strict_args_position!
@@ -231,7 +233,7 @@ module Rails
# Invoke source_root so the default_source_root is set.
base.source_root
- if base.name && base.name !~ /Base$/
+ if base.name && !base.name.match?(/Base$/)
Rails::Generators.subclasses << base
Rails::Generators.templates_path.each do |path|
@@ -249,6 +251,7 @@ module Rails
# application or Ruby on Rails.
def class_collisions(*class_names)
return unless behavior == :invoke
+ return if options.skip_collision_check?
class_names.flatten.each do |class_name|
class_name = class_name.to_s
@@ -261,8 +264,8 @@ module Rails
if last && last.const_defined?(last_name.camelize, false)
raise Error, "The name '#{class_name}' is either already used in your application " \
- "or reserved by Ruby on Rails. Please choose an alternative and run " \
- "this generator again."
+ "or reserved by Ruby on Rails. Please choose an alternative or use --skip-collision-check " \
+ "to skip this check and run this generator again."
end
end
end
diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb
index 4e348be9be..377a5dfc65 100644
--- a/railties/lib/rails/generators/generated_attribute.rb
+++ b/railties/lib/rails/generators/generated_attribute.rb
@@ -131,7 +131,7 @@ module Rails
end
def foreign_key?
- !!(name =~ /_id$/)
+ /_id$/.match?(name)
end
def reference?
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
index cf5462f7dc..f13dab59b1 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
@@ -72,7 +72,7 @@ group :test do
# Easy installation and use of web drivers to run system tests with browsers
gem 'webdrivers'
end
-<%- end -%>
+<%- end -%>
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
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 b8c1f21c0b..437bf84ce3 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
@@ -2,6 +2,7 @@
<html>
<head>
<title><%= camelized %></title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
<%%= csrf_meta_tags %>
<%%= csp_meta_tag %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
index c66e349442..41dabb87df 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
@@ -3,6 +3,8 @@
# your test database is "scratch space" for the test suite and is wiped
# and recreated between test runs. Don't rely on the data there!
+require "active_support/core_ext/integer/time"
+
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
<%# Spring executes the reloaders when files change. %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
index 59385cdf37..3c56b21b3c 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/backtrace_silencers.rb.tt
@@ -1,7 +1,7 @@
# Be sure to restart your server when you modify this file.
# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
+# Rails.backtrace_cleaner.add_silencer { |line| /my_noisy_library/.match?(line) }
# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
# Rails.backtrace_cleaner.remove_silencers!
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 c517b0f96b..3d468f7633 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
@@ -23,6 +23,9 @@
# If you are using UJS then enable automatic nonce generation
# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) }
+# Set the nonce only to specific directives
+# Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
+
# 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/feature_policy.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/feature_policy.rb.tt
new file mode 100644
index 0000000000..a1c46695d2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/feature_policy.rb.tt
@@ -0,0 +1,11 @@
+# Define an application-wide HTTP feature policy. For further
+# information see https://developers.google.com/web/updates/2018/06/feature-policy
+#
+# Rails.application.config.feature_policy do |f|
+# f.camera :none
+# f.gyroscope :none
+# f.microphone :none
+# f.usb :none
+# f.fullscreen :self
+# f.payment :self, "https://secure.example.com"
+# end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
index 4a994e1e7b..eea99edb65 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt
@@ -1,4 +1,6 @@
# Be sure to restart your server when you modify this file.
# Configure sensitive parameters which will be filtered from the log file.
-Rails.application.config.filter_parameters += [:password]
+Rails.application.config.filter_parameters += [
+ :password, :secret, :token, :_key, :auth, :crypt, :salt, :certificate, :otp, :access, :private, :protected, :ssn
+]
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
index abb03e761b..2510ab906f 100644
--- 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
@@ -26,6 +26,10 @@
# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis
# Rails.application.config.active_storage.queues.purge = :active_storage_purge
+# When assigning to a collection of attachments declared via `has_many_attached`, replace existing
+# attachments instead of appending. Use #attach to add new attachments without replacing existing ones.
+# Rails.application.config.active_storage.replace_on_assign_to_many = true
+
# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail.
#
# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob),
@@ -34,3 +38,8 @@
# MailDeliveryJob to ensure all delivery jobs are processed properly.
# Make sure your entire app is migrated and stable on 6.0 before using this setting.
# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob"
+
+# Enable the same cache key to be reused when the object being cached of type
+# `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count)
+# of the relation's cache key into the cache version to support recycling cache key.
+# Rails.application.config.active_record.collection_cache_versioning = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
index 649253aeca..5ed4437744 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt
@@ -16,6 +16,9 @@ port ENV.fetch("PORT") { 3000 }
#
environment ENV.fetch("RAILS_ENV") { "development" }
+# Specifies the `pidfile` that Puma will use.
+pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
+
# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
diff --git a/railties/lib/rails/generators/rails/app/templates/public/robots.txt b/railties/lib/rails/generators/rails/app/templates/public/robots.txt
index 37b576a4a0..c19f78ab68 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/robots.txt
+++ b/railties/lib/rails/generators/rails/app/templates/public/robots.txt
@@ -1 +1 @@
-# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index 838fe55acc..0664338e0b 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -223,12 +223,10 @@ module Rails
private
def files_in(path)
- Dir.chdir(path) do
- files = Dir.glob(@glob)
- files -= @exclude if @exclude
- files.map! { |file| File.join(path, file) }
- files.sort
- end
+ files = Dir.glob(@glob, base: path)
+ files -= @exclude if @exclude
+ files.map! { |file| File.join(path, file) }
+ files.sort
end
end
end
diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb
index 9ce22b96a6..77a99036ec 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -2,11 +2,6 @@
require "active_support/deprecation"
-# Remove this deprecated class in the next minor version
-#:nodoc:
-SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy.
- new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor")
-
module Rails
# Implements the logic behind <tt>Rails::Command::NotesCommand</tt>. See <tt>rails notes --help</tt> for usage information.
#
@@ -160,3 +155,8 @@ module Rails
end
end
end
+
+# Remove this deprecated class in the next minor version
+#:nodoc:
+SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy.
+ new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor")
diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake
index 2886986865..c0f19fcfe5 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -8,7 +8,7 @@ namespace :app do
task template: :environment do
template = ENV["LOCATION"]
raise "No LOCATION value given. Please set LOCATION either as path to a file or a URL" if template.blank?
- template = File.expand_path(template) if template !~ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://}
+ template = File.expand_path(template) unless %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://}.match?(template)
require "rails/generators"
require "rails/generators/rails/app/app_generator"
generator = Rails::Generators::AppGenerator.new [Rails.root], {}, { destination_root: Rails.root }
diff --git a/railties/lib/rails/tasks/zeitwerk.rake b/railties/lib/rails/tasks/zeitwerk.rake
index e748a479a7..5421af6e8b 100644
--- a/railties/lib/rails/tasks/zeitwerk.rake
+++ b/railties/lib/rails/tasks/zeitwerk.rake
@@ -1,62 +1,14 @@
# frozen_string_literal: true
-indent = " " * 2
-
-ensure_classic_mode = ->() do
- if Rails.autoloaders.zeitwerk_enabled?
- abort <<~EOS
- Please, enable temporarily :classic mode:
-
- # config/application.rb
- config.autoloader = :classic
-
- and try again. When all is good, you can delete that line.
- EOS
+ensure_zeitwerk_mode = ->() do
+ unless Rails.autoloaders.zeitwerk_enabled?
+ abort "Please, enable :zeitwerk mode in config/application.rb and try again."
end
end
eager_load = ->() do
- Rails.configuration.eager_load_namespaces.each(&:eager_load!)
-end
-
-check_directory = ->(directory, parent, mismatches) do
- # test/mailers/previews might not exist.
- return unless File.exist?(directory)
-
- Dir.foreach(directory) do |entry|
- next if entry.start_with?(".")
- next if parent == Object && entry == "concerns"
-
- abspath = File.join(directory, entry)
-
- if File.directory?(abspath) || abspath.end_with?(".rb")
- print "."
- cname = File.basename(abspath, ".rb").camelize.to_sym
- if parent.const_defined?(cname, false)
- if File.directory?(abspath)
- check_directory[abspath, parent.const_get(cname), mismatches]
- end
- else
- mismatches << [abspath, parent, cname]
- end
- end
- end
-end
-
-report_mismatches = ->(mismatches) do
- puts
- rails_root_prefix_re = %r{\A#{Regexp.escape(Rails.root.to_path)}/}
- mismatches.each do |abspath, parent, cname|
- relpath = abspath.sub(rails_root_prefix_re, "")
- cpath = parent == Object ? cname : "#{parent.name}::#{cname}"
- puts indent + "Mismatch: Expected #{relpath} to define #{cpath}"
- end
- puts
-
- puts <<~EOS
- Please revise the reported mismatches. You can normally fix them by adding
- acronyms to config/initializers/inflections.rb or renaming the constants.
- EOS
+ puts "Hold on, I am eager loading the application."
+ Zeitwerk::Loader.eager_load_all
end
report_not_checked = ->(not_checked) do
@@ -67,47 +19,48 @@ report_not_checked = ->(not_checked) do
EOS
puts
- not_checked.each { |dir| puts indent + dir }
+ not_checked.each { |dir| puts " #{dir}" }
puts
puts <<~EOS
You may verify them manually, or add them to config.eager_load_paths
in config/application.rb and run zeitwerk:check again.
EOS
+ puts
end
-report = ->(mismatches, not_checked) do
- puts
- if mismatches.empty? && not_checked.empty?
- puts "All is good!"
- puts "Please, remember to delete `config.autoloader = :classic` from config/application.rb."
+report = ->(not_checked) do
+ if not_checked.any?
+ report_not_checked[not_checked]
+ puts "Otherwise, all is good!"
else
- report_mismatches[mismatches] if mismatches.any?
- report_not_checked[not_checked] if not_checked.any?
+ puts "All is good!"
end
end
namespace :zeitwerk do
desc "Checks project structure for Zeitwerk compatibility"
task check: :environment do
- ensure_classic_mode[]
- eager_load[]
+ ensure_zeitwerk_mode[]
+
+ begin
+ eager_load[]
+ rescue NameError => e
+ if e.message =~ /expected file .*? to define constant \S+/
+ abort $&.sub(/#{Regexp.escape(Rails.root.to_s)}./, "")
+ else
+ raise
+ end
+ end
eager_load_paths = Rails.configuration.eager_load_namespaces.map do |eln|
eln.config.eager_load_paths if eln.respond_to?(:config)
end.compact.flatten
- mismatches = []
-
- $stdout.sync = true
- eager_load_paths.each do |eager_load_path|
- check_directory[eager_load_path, Object, mismatches]
- end
-
not_checked = ActiveSupport::Dependencies.autoload_paths - eager_load_paths
not_checked.select! { |dir| Dir.exist?(dir) }
not_checked.reject! { |dir| Dir.empty?(dir) }
- report[mismatches, not_checked]
+ report[not_checked]
end
end
diff --git a/railties/lib/rails/test_unit/runner.rb b/railties/lib/rails/test_unit/runner.rb
index 7b294751fc..b8bce8c772 100644
--- a/railties/lib/rails/test_unit/runner.rb
+++ b/railties/lib/rails/test_unit/runner.rb
@@ -61,7 +61,7 @@ module Rails
private
def extract_filters(argv)
# Extract absolute and relative paths but skip -n /.*/ regexp filters.
- argv.select { |arg| arg =~ %r%^/?\w+/% && !arg.end_with?("/") }.map do |path|
+ argv.select { |arg| %r%^/?\w+/%.match?(arg) && !arg.end_with?("/") }.map do |path|
case
when /(:\d+)+$/.match?(path)
file, *lines = path.split(":")
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index a7dd233f3d..651897d8f3 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -311,7 +311,7 @@ module ApplicationTests
manifest = Dir["#{app_path}/public/assets/.sprockets-manifest-*.json"].first
assets = ActiveSupport::JSON.decode(File.read(manifest))
- assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1]
+ assert asset_path = assets["assets"].find { |(k, _)| /.png/.match?(k) }[1]
# Load app env
app "development"
diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb
index aa0da0931d..d84ab61cf9 100644
--- a/railties/test/application/bin_setup_test.rb
+++ b/railties/test/application/bin_setup_test.rb
@@ -31,7 +31,7 @@ module ApplicationTests
Dir.chdir(app_path) do
# SQLite3 seems to auto-create the database on first checkout.
rails "db:system:change", "--to=postgresql"
- rails "db:drop"
+ rails "db:drop", allow_failure: true
app_file "db/schema.rb", ""
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index f6bec3242a..b44edbc7d3 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -456,7 +456,7 @@ module ApplicationTests
test "filter_parameters should be able to set via config.filter_parameters" do
add_to_config <<-RUBY
config.filter_parameters += [ :foo, 'bar', lambda { |key, value|
- value = value.reverse if key =~ /baz/
+ value = value.reverse if /baz/.match?(key)
}]
RUBY
@@ -790,7 +790,7 @@ module ApplicationTests
end
get "/"
- assert last_response.body =~ /csrf\-param/
+ assert_match(/csrf\-param/, last_response.body)
end
test "default form builder specified as a string" do
@@ -1708,7 +1708,7 @@ module ApplicationTests
app "development"
ActiveSupport::Dependencies.autoload_paths.each do |path|
assert_not_operator path, :ends_with?, "app/assets"
- assert_not_operator path, :ends_with?, "app/javascript"
+ assert_not_operator path, :ends_with?, "app/#{Rails.configuration.webpacker_path}"
end
end
@@ -1793,6 +1793,11 @@ module ApplicationTests
assert_equal [X, D], C.descendants
end
+ test "load_database_yaml returns blank hash if configuration file is blank" do
+ app_file "config/database.yml", ""
+ app "development"
+ assert_equal({}, Rails.application.config.load_database_yaml)
+ end
test "raises with proper error message if no database configuration found" do
FileUtils.rm("#{app_path}/config/database.yml")
@@ -2588,6 +2593,21 @@ module ApplicationTests
MESSAGE
end
+ test "ActiveStorage.draw_routes can be configured via config.active_storage.draw_routes" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.active_storage.draw_routes = false
+ end
+ RUBY
+
+ output = rails("routes")
+ assert_not_includes(output, "rails_service_blob")
+ assert_not_includes(output, "rails_blob_representation")
+ assert_not_includes(output, "rails_disk_service")
+ assert_not_includes(output, "update_rails_disk_service")
+ assert_not_includes(output, "rails_direct_uploads")
+ end
+
test "hosts include .localhost in development" do
app "development"
assert_includes Rails.application.config.hosts, ".localhost"
diff --git a/railties/test/application/content_security_policy_test.rb b/railties/test/application/content_security_policy_test.rb
index 3338bcb47d..0bb6ee917a 100644
--- a/railties/test/application/content_security_policy_test.rb
+++ b/railties/test/application/content_security_policy_test.rb
@@ -119,6 +119,38 @@ module ApplicationTests
assert_policy "default-src 'self' https:", report_only: true
end
+ test "global content security policy nonce directives in an initializer" 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|
+ p.default_src :self, :https
+ p.script_src :self, :https
+ p.style_src :self, :https
+ end
+
+ Rails.application.config.content_security_policy_nonce_generator = proc { "iyhD0Yc0W+c=" }
+ Rails.application.config.content_security_policy_nonce_directives = %w(script-src)
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "default-src 'self' https:; script-src 'self' https: 'nonce-iyhD0Yc0W+c='; style-src 'self' https:"
+ end
+
test "override content security policy in a controller" do
controller :pages, <<-RUBY
class PagesController < ApplicationController
diff --git a/railties/test/application/current_attributes_integration_test.rb b/railties/test/application/current_attributes_integration_test.rb
index 146e96facc..f4ff5b8077 100644
--- a/railties/test/application/current_attributes_integration_test.rb
+++ b/railties/test/application/current_attributes_integration_test.rb
@@ -18,7 +18,7 @@ class CurrentAttributesIntegrationTest < ActiveSupport::TestCase
def customer=(customer)
super
- Time.zone = customer.try(:time_zone)
+ Time.zone = customer&.time_zone
end
end
RUBY
@@ -53,7 +53,7 @@ class CurrentAttributesIntegrationTest < ActiveSupport::TestCase
RUBY
app_file "app/views/customers/index.html.erb", <<-RUBY
- <%= Current.customer.try(:name) || 'noone' %>,<%= Time.zone.name %>
+ <%= Current.customer&.name || 'noone' %>,<%= Time.zone.name %>
RUBY
require "#{app_path}/config/environment"
diff --git a/railties/test/application/feature_policy_test.rb b/railties/test/application/feature_policy_test.rb
new file mode 100644
index 0000000000..e751d782ee
--- /dev/null
+++ b/railties/test/application/feature_policy_test.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+
+module ApplicationTests
+ class FeaturePolicyTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "feature policy is not enabled by default" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_nil last_response.headers["Feature-Policy"]
+ end
+
+ test "global feature policy in an initializer" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/feature_policy.rb", <<-RUBY
+ Rails.application.config.feature_policy do |p|
+ p.geolocation :none
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "geolocation 'none'"
+ end
+
+ test "override feature policy using same directive in a controller" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ feature_policy do |p|
+ p.geolocation "https://example.com"
+ end
+
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/feature_policy.rb", <<-RUBY
+ Rails.application.config.feature_policy do |p|
+ p.geolocation :none
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "geolocation https://example.com"
+ end
+
+ test "override feature policy by unsetting a directive in a controller" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ feature_policy do |p|
+ p.geolocation nil
+ end
+
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/feature_policy.rb", <<-RUBY
+ Rails.application.config.feature_policy do |p|
+ p.geolocation :none
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_equal 200, last_response.status
+ assert_nil last_response.headers["Feature-Policy"]
+ end
+
+ test "override feature policy using different directives in a controller" do
+ controller :pages, <<-RUBY
+ class PagesController < ApplicationController
+ feature_policy do |p|
+ p.geolocation nil
+ p.payment "https://secure.example.com"
+ p.autoplay :none
+ end
+
+ def index
+ render html: "<h1>Welcome to Rails!</h1>"
+ end
+ end
+ RUBY
+
+ app_file "config/initializers/feature_policy.rb", <<-RUBY
+ Rails.application.config.feature_policy do |p|
+ p.geolocation :none
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ root to: "pages#index"
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "payment https://secure.example.com; autoplay 'none'"
+ end
+
+ test "global feature policy added to rack app" do
+ app_file "config/initializers/feature_policy.rb", <<-RUBY
+ Rails.application.config.feature_policy do |p|
+ p.payment :none
+ end
+ RUBY
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ app = ->(env) {
+ [200, { "Content-Type" => "text/html" }, ["<p>Hello, World!</p>"]]
+ }
+ root to: app
+ end
+ RUBY
+
+ app("development")
+
+ get "/"
+ assert_policy "payment 'none'"
+ end
+
+ private
+ def assert_policy(expected)
+ assert_equal 200, last_response.status
+ assert_equal expected, last_response.headers["Feature-Policy"]
+ end
+ end
+end
diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb
index e5e557d204..8ec26db772 100644
--- a/railties/test/application/generators_test.rb
+++ b/railties/test/application/generators_test.rb
@@ -198,5 +198,15 @@ module ApplicationTests
assert_no_match "active_record:migration", output
end
end
+
+ test "skip collision check" do
+ rails("generate", "model", "post", "title:string")
+
+ output = rails("generate", "model", "post", "title:string", "body:string")
+ assert_match(/The name 'Post' is either already used in your application or reserved/, output)
+
+ output = rails("generate", "model", "post", "title:string", "body:string", "--skip-collision-check")
+ assert_no_match(/The name 'Post' is either already used in your application or reserved/, output)
+ end
end
end
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 54c84e2e7c..e93f2f5aa4 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -46,6 +46,7 @@ module ApplicationTests
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
"ActionDispatch::ContentSecurityPolicy::Middleware",
+ "ActionDispatch::FeaturePolicy::Middleware",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag",
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index e08cd09abd..c9931c45a6 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -40,6 +40,15 @@ module ApplicationTests
end
end
+ def db_create_with_warning(expected_database)
+ Dir.chdir(app_path) do
+ output = rails("db:create")
+ assert_match(/Rails couldn't infer whether you are using multiple databases/, output)
+ assert_match(/Created database/, output)
+ assert File.exist?(expected_database)
+ end
+ end
+
test "db:create and db:drop without database URL" do
require "#{app_path}/config/environment"
db_create_and_drop ActiveRecord::Base.configurations[Rails.env]["database"]
@@ -86,6 +95,25 @@ module ApplicationTests
db_create_and_drop("db/development.sqlite3", environment_loaded: false)
end
+ test "db:create and db:drop show warning but doesn't raise errors when loading YAML with alias ERB" do
+ app_file "config/database.yml", <<-YAML
+ sqlite: &sqlite
+ adapter: sqlite3
+ database: db/development.sqlite3
+
+ development:
+ <<: *<%= ENV["DB"] || "sqlite" %>
+ YAML
+
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.database = "db/development.sqlite3"
+ end
+ RUBY
+
+ db_create_with_warning("db/development.sqlite3")
+ end
+
test "db:create and db:drop don't raise errors when loading YAML containing conditional statements in ERB" do
app_file "config/database.yml", <<-YAML
development:
@@ -602,6 +630,22 @@ module ApplicationTests
assert_match(/CreateRecipes: migrated/, output)
end
end
+
+ test "db:prepare does not touch schema when dumping is disabled" do
+ Dir.chdir(app_path) do
+ rails "generate", "model", "book", "title:string"
+ rails "db:create", "db:migrate"
+
+ app_file "db/schema.rb", "Not touched"
+ app_file "config/initializers/disable_dumping_schema.rb", <<-RUBY
+ Rails.application.config.active_record.dump_schema_after_migration = false
+ RUBY
+
+ rails "db:prepare"
+
+ assert_equal("Not touched", File.read("db/schema.rb").strip)
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb
index 2606e64424..c5f8904f4e 100644
--- a/railties/test/application/rake/multi_dbs_test.rb
+++ b/railties/test/application/rake/multi_dbs_test.rb
@@ -349,6 +349,25 @@ module ApplicationTests
rails "db:drop" rescue nil
end
end
+
+ test "db:seed uses primary database connection" do
+ @old_rails_env = ENV["RAILS_ENV"]
+ @old_rack_env = ENV["RACK_ENV"]
+ ENV.delete "RAILS_ENV"
+ ENV.delete "RACK_ENV"
+
+ db_migrate_and_schema_dump_and_load "schema"
+
+ app_file "db/seeds.rb", <<-RUBY
+ print Book.connection.pool.spec.config[:database]
+ RUBY
+
+ output = rails("db:seed")
+ assert_equal output, "db/development.sqlite3"
+ ensure
+ ENV["RAILS_ENV"] = @old_rails_env
+ ENV["RACK_ENV"] = @old_rack_env
+ end
end
end
end
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index fe56e3d076..e8456e8c19 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -162,7 +162,6 @@ module ApplicationTests
rails "generate", "scaffold", "user", "username:string", "password:string"
with_rails_env("test") do
rails("db:migrate")
- rails("webpacker:compile")
end
output = rails("test")
@@ -194,7 +193,6 @@ module ApplicationTests
rails "generate", "scaffold", "LineItems", "product:references", "cart:belongs_to"
with_rails_env("test") do
rails("db:migrate")
- rails("webpacker:compile")
end
output = rails("test")
diff --git a/railties/test/application/system_test_case_test.rb b/railties/test/application/system_test_case_test.rb
new file mode 100644
index 0000000000..d15a0d9210
--- /dev/null
+++ b/railties/test/application/system_test_case_test.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "rack/test"
+
+class SystemTestCaseTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "url helpers are delegated to a proxy class" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index', as: 'test_foo'
+ end
+ RUBY
+
+ app("test")
+
+ assert_not_includes(ActionDispatch::SystemTestCase.runnable_methods, :test_foo_url)
+ end
+
+ test "system tests set the Capybara host in the url_options by default" do
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index', as: 'test_foo'
+ end
+ RUBY
+
+ app("test")
+ system_test = ActionDispatch::SystemTestCase.new("my_test")
+ previous_app_host = ::Capybara.app_host
+ ::Capybara.app_host = "https://my_test_example.com"
+
+ assert_equal("https://my_test_example.com/foo", system_test.test_foo_url)
+ ensure
+ ::Capybara.app_host = previous_app_host
+ end
+end
diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb
index 9146222f73..d03fa3d0c6 100644
--- a/railties/test/application/zeitwerk_integration_test.rb
+++ b/railties/test/application/zeitwerk_integration_test.rb
@@ -98,6 +98,15 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert_nil deps.safe_constantize("Admin")
end
+ test "autoloaded? and overridden class names" do
+ invalid_constant_name = Module.new do
+ def self.name
+ "primary::SchemaMigration"
+ end
+ end
+ assert_not deps.autoloaded?(invalid_constant_name)
+ end
+
test "unloadable constants (main)" do
app_file "app/models/user.rb", "class User; end"
app_file "app/models/post.rb", "class Post; end"
@@ -141,6 +150,35 @@ class ZeitwerkIntegrationTest < ActiveSupport::TestCase
assert_empty deps.autoloaded_constants
end
+ [true, false].each do |add_aps_to_lp|
+ test "require_dependency looks autoload paths up (#{add_aps_to_lp})" do
+ add_to_config "config.add_autoload_paths_to_load_path = #{add_aps_to_lp}"
+ app_file "app/models/user.rb", "class User; end"
+ boot
+
+ assert require_dependency("user")
+ end
+
+ test "require_dependency handles absolute paths correctly (#{add_aps_to_lp})" do
+ add_to_config "config.add_autoload_paths_to_load_path = #{add_aps_to_lp}"
+ app_file "app/models/user.rb", "class User; end"
+ boot
+
+ assert require_dependency("#{app_path}/app/models/user.rb")
+ end
+
+ test "require_dependency supports arguments that repond to to_path (#{add_aps_to_lp})" do
+ add_to_config "config.add_autoload_paths_to_load_path = #{add_aps_to_lp}"
+ app_file "app/models/user.rb", "class User; end"
+ boot
+
+ user = Object.new
+ def user.to_path; "user"; end
+
+ assert require_dependency(user)
+ end
+ end
+
test "eager loading loads the application code" do
$zeitwerk_integration_test_user = false
$zeitwerk_integration_test_post = false
diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb
index 0ee36081c0..974b34836b 100644
--- a/railties/test/commands/credentials_test.rb
+++ b/railties/test/commands/credentials_test.rb
@@ -4,13 +4,14 @@ require "isolation/abstract_unit"
require "env_helpers"
require "rails/command"
require "rails/commands/credentials/credentials_command"
+require "fileutils"
+require "tempfile"
class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation, EnvHelpers
- setup { build_app }
-
- teardown { teardown_app }
+ setup :build_app
+ teardown :teardown_app
test "edit without editor gives hint" do
run_edit_command(editor: "").tap do |output|
@@ -88,6 +89,7 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
assert_match(/secret_key_base/, output)
end
+
test "show credentials" do
assert_match(/access_key_id: 123/, run_show_command)
end
@@ -120,6 +122,63 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
assert_no_match(/secret_key_base/, output)
end
+
+ test "diff enroll diffing" do
+ assert_match("successfully enrolled", run_diff_command(enroll: true))
+
+ assert_equal <<~EOM, File.read(app_path(".gitattributes"))
+ config/credentials/*.yml.enc diff=rails_credentials
+ config/credentials.yml.enc diff=rails_credentials
+ EOM
+ end
+
+ test "running edit after enrolling in diffing sets diff driver" do
+ run_diff_command(enroll: true)
+ run_edit_command
+
+ Dir.chdir(app_path) do
+ assert_equal "bin/rails credentials:diff", `git config --get 'diff.rails_credentials.textconv'`.strip
+ end
+ end
+
+ test "diff from git diff left file" do
+ run_edit_command(environment: "development")
+
+ assert_match(/access_key_id: 123/, run_diff_command("config/credentials/development.yml.enc"))
+ end
+
+ test "diff from git diff right file" do
+ run_edit_command(environment: "development")
+
+ content_path = app_path("config", "credentials", "KnAM4a_development.yml.enc")
+ File.write(content_path,
+ File.read(app_path("config", "credentials", "development.yml.enc")))
+
+ assert_match(/access_key_id: 123/, run_diff_command(content_path))
+ end
+
+ test "diff for main credentials" do
+ assert_match(/access_key_id: 123/, run_diff_command("config/credentials.yml.enc"))
+ end
+
+ test "diff when master key is not available" do
+ remove_file "config/master.key"
+
+ raw_content = File.read(app_path("config", "credentials.yml.enc"))
+ assert_match(raw_content, run_diff_command("config/credentials.yml.enc"))
+ end
+
+ test "diff returns raw encrypted content when errors occur" do
+ run_edit_command(environment: "development")
+
+ content_path = app_path("20190807development.yml.enc")
+ encrypted_content = File.read(app_path("config", "credentials", "development.yml.enc"))
+ File.write(content_path, encrypted_content + "ruin decryption")
+
+ assert_match(encrypted_content, run_diff_command(content_path))
+ end
+
+
private
def run_edit_command(editor: "cat", environment: nil, **options)
switch_env("EDITOR", editor) do
@@ -132,4 +191,9 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
args = environment ? ["--environment", environment] : []
rails "credentials:show", args, **options
end
+
+ def run_diff_command(path = nil, enroll: nil, **options)
+ args = enroll ? ["--enroll"] : [path]
+ rails "credentials:diff", args, **options
+ end
end
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 150836d4ce..5d6d7f1595 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -43,7 +43,7 @@ class ActionsTest < Rails::Generators::TestCase
def test_add_source_adds_source_to_gemfile
run_generator
action :add_source, "http://gems.github.com"
- assert_file "Gemfile", /source 'http:\/\/gems\.github\.com'/
+ assert_file "Gemfile", /source 'http:\/\/gems\.github\.com'\n/
end
def test_add_source_with_block_adds_source_to_gemfile_with_gem
@@ -51,7 +51,7 @@ class ActionsTest < Rails::Generators::TestCase
action :add_source, "http://gems.github.com" do
gem "rspec-rails"
end
- assert_file "Gemfile", /source 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/
+ assert_file "Gemfile", /\n\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend\n\z/
end
def test_add_source_with_block_adds_source_to_gemfile_after_gem
@@ -60,13 +60,25 @@ class ActionsTest < Rails::Generators::TestCase
action :add_source, "http://gems.github.com" do
gem "rspec-rails"
end
- assert_file "Gemfile", /gem 'will-paginate'\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend/
+ assert_file "Gemfile", /\ngem 'will-paginate'\n\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend\n\z/
+ end
+
+ def test_add_source_should_create_newline_between_blocks
+ run_generator
+ action :add_source, "http://gems.github.com" do
+ gem "rspec-rails"
+ end
+
+ action :add_source, "http://gems2.github.com" do
+ gem "fakeweb"
+ end
+ assert_file "Gemfile", /\n\nsource 'http:\/\/gems\.github\.com' do\n gem 'rspec-rails'\nend\n\nsource 'http:\/\/gems2\.github\.com' do\n gem 'fakeweb'\nend\n\z/
end
def test_gem_should_put_gem_dependency_in_gemfile
run_generator
action :gem, "will-paginate"
- assert_file "Gemfile", /gem 'will\-paginate'/
+ assert_file "Gemfile", /gem 'will\-paginate'\n\z/
end
def test_gem_with_version_should_include_version_in_gemfile
@@ -141,7 +153,7 @@ class ActionsTest < Rails::Generators::TestCase
gem "fakeweb"
end
- assert_file "Gemfile", /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/
+ assert_file "Gemfile", /\n\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend\n\z/
end
def test_github_should_create_an_indented_block
@@ -153,7 +165,7 @@ class ActionsTest < Rails::Generators::TestCase
gem "baz"
end
- assert_file "Gemfile", /\ngithub 'user\/repo' do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/
+ assert_file "Gemfile", /\n\ngithub 'user\/repo' do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend\n\z/
end
def test_github_should_create_an_indented_block_with_options
@@ -165,7 +177,7 @@ class ActionsTest < Rails::Generators::TestCase
gem "baz"
end
- assert_file "Gemfile", /\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend/
+ assert_file "Gemfile", /\n\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend\n\z/
end
def test_github_should_create_an_indented_block_within_a_group
@@ -177,9 +189,73 @@ class ActionsTest < Rails::Generators::TestCase
gem "bar"
gem "baz"
end
+ github "user/repo2", a: "correct", other: true do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+ end
+
+ assert_file "Gemfile", /\n\ngroup :magic do\n github 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\n github 'user\/repo2', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\nend\n\z/
+ end
+
+ def test_github_should_create_newline_between_blocks
+ run_generator
+
+ action :github, "user/repo", a: "correct", other: true do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+
+ action :github, "user/repo2", a: "correct", other: true do
+ gem "foo"
+ gem "bar"
+ gem "baz"
+ end
+
+ assert_file "Gemfile", /\n\ngithub 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend\n\ngithub 'user\/repo2', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\nend\n\z/
+ end
+
+ def test_gem_with_gemfile_without_newline_at_the_end
+ run_generator
+ File.open("Gemfile", "a") { |f| f.write("gem 'rspec-rails'") }
+
+ action :gem, "will-paginate"
+ assert_file "Gemfile", /gem 'rspec-rails'\ngem 'will-paginate'\n\z/
+ end
+
+ def test_gem_group_with_gemfile_without_newline_at_the_end
+ run_generator
+ File.open("Gemfile", "a") { |f| f.write("gem 'rspec-rails'") }
+
+ action :gem_group, :test do
+ gem "fakeweb"
+ end
+
+ assert_file "Gemfile", /gem 'rspec-rails'\n\ngroup :test do\n gem 'fakeweb'\nend\n\z/
+ end
+
+ def test_add_source_with_gemfile_without_newline_at_the_end
+ run_generator
+ File.open("Gemfile", "a") { |f| f.write("gem 'rspec-rails'") }
+
+ action :add_source, "http://gems.github.com" do
+ gem "fakeweb"
+ end
+
+ assert_file "Gemfile", /gem 'rspec-rails'\n\nsource 'http:\/\/gems\.github\.com' do\n gem 'fakeweb'\nend\n\z/
+ end
+
+ def test_github_with_gemfile_without_newline_at_the_end
+ run_generator
+ File.open("Gemfile", "a") { |f| f.write("gem 'rspec-rails'") }
+
+ action :github, "user/repo" do
+ gem "fakeweb"
end
- assert_file "Gemfile", /\ngroup :magic do\n github 'user\/repo', a: 'correct', other: true do\n gem 'foo'\n gem 'bar'\n gem 'baz'\n end\nend\n/
+ assert_file "Gemfile", /gem 'rspec-rails'\n\ngithub 'user\/repo' do\n gem 'fakeweb'\nend\n\z/
end
def test_environment_should_include_data_in_environment_initializer_block
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index 5b439fdcba..43461036f3 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -678,7 +678,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_inclusion_of_listen_related_configuration_by_default
run_generator
- if RbConfig::CONFIG["host_os"] =~ /darwin|linux/
+ if /darwin|linux/.match?(RbConfig::CONFIG["host_os"])
assert_listen_related_configuration
else
assert_no_listen_related_configuration
@@ -690,7 +690,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
Object.const_set(:RUBY_ENGINE, "MyRuby")
run_generator
- if RbConfig::CONFIG["host_os"] =~ /darwin|linux/
+ if /darwin|linux/.match?(RbConfig::CONFIG["host_os"])
assert_listen_related_configuration
else
assert_no_listen_related_configuration
@@ -708,7 +708,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_evented_file_update_checker_config
run_generator
assert_file "config/environments/development.rb" do |content|
- if RbConfig::CONFIG["host_os"] =~ /darwin|linux/
+ if /darwin|linux/.match?(RbConfig::CONFIG["host_os"])
assert_match(/^\s*config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
else
assert_match(/^\s*# config\.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb
index 4b75a31f17..12ccbe6ff1 100644
--- a/railties/test/generators/namespaced_generators_test.rb
+++ b/railties/test/generators/namespaced_generators_test.rb
@@ -308,7 +308,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
run_generator [ "admin/role" ], behavior: :revoke
# Model
- assert_file "app/models/test_app/admin.rb" # ( should not be remove )
+ assert_file "app/models/test_app/admin.rb" # ( should not be remove )
assert_no_file "app/models/test_app/admin/role.rb"
assert_no_file "test/models/test_app/admin/role_test.rb"
assert_no_file "test/fixtures/test_app/admin/roles.yml"
@@ -375,7 +375,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
run_generator [ "admin/user/special/role" ], behavior: :revoke
# Model
- assert_file "app/models/test_app/admin/user/special.rb" # ( should not be remove )
+ assert_file "app/models/test_app/admin/user/special.rb" # ( should not be remove )
assert_no_file "app/models/test_app/admin/user/special/role.rb"
assert_no_file "test/models/test_app/admin/user/special/role_test.rb"
assert_no_file "test/fixtures/test_app/admin/user/special/roles.yml"
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index 7f05b9d9cf..06836844f0 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -110,7 +110,7 @@ module RailtiesTest
assert_no_match(/\d+_create_users/, output.join("\n"))
- bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/ =~ o })
+ bukkits_migration_order = output.index(output.detect { |o| /NOTE: Migration \d+_create_sessions\.rb from bukkits has been skipped/.match?(o) })
assert_not_nil bukkits_migration_order, "Expected migration to be skipped"
end
end