aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml3
-rw-r--r--.travis.yml119
-rw-r--r--Gemfile9
-rw-r--r--Gemfile.lock62
-rw-r--r--actioncable/CHANGELOG.md61
-rw-r--r--actioncable/Rakefile4
-rw-r--r--actioncable/lib/action_cable/channel/base.rb2
-rw-r--r--actioncable/lib/action_cable/channel/broadcasting.rb26
-rw-r--r--actioncable/lib/action_cable/channel/streams.rb2
-rw-r--r--actioncable/lib/action_cable/channel/test_case.rb10
-rw-r--r--actioncable/lib/action_cable/connection/base.rb2
-rw-r--r--actioncable/lib/action_cable/connection/test_case.rb2
-rw-r--r--actioncable/lib/action_cable/server/base.rb7
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt6
-rw-r--r--actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt4
-rw-r--r--actioncable/test/channel/broadcasting_test.rb15
-rw-r--r--actioncable/test/channel/test_case_test.rb2
-rw-r--r--actioncable/test/subscription_adapter/channel_prefix.rb10
-rw-r--r--actioncable/test/subscription_adapter/redis_test.rb8
-rw-r--r--actionmailbox/CHANGELOG.md6
-rw-r--r--actionmailbox/app/jobs/action_mailbox/incineration_job.rb3
-rw-r--r--actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb2
-rw-r--r--actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb6
-rw-r--r--actionmailbox/lib/action_mailbox.rb1
-rw-r--r--actionmailbox/lib/action_mailbox/engine.rb2
-rw-r--r--actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb6
-rw-r--r--actionmailbox/test/dummy/db/schema.rb1
-rw-r--r--actionmailbox/test/unit/inbound_email/incineration_test.rb10
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb16
-rw-r--r--actionpack/CHANGELOG.md14
-rw-r--r--actionpack/lib/abstract_controller/caching/fragments.rb1
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb2
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb2
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb2
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_view.rb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb24
-rw-r--r--actionpack/lib/action_dispatch/middleware/host_authorization.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb2
-rw-r--r--actionpack/lib/action_dispatch/routing.rb28
-rw-r--r--actionpack/lib/action_dispatch/system_test_case.rb24
-rw-r--r--actionpack/lib/action_dispatch/system_testing/browser.rb22
-rw-r--r--actionpack/lib/action_dispatch/system_testing/driver.rb9
-rw-r--r--actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb2
-rw-r--r--actionpack/test/controller/redirect_test.rb29
-rw-r--r--actionpack/test/dispatch/cookies_test.rb11
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb63
-rw-r--r--actionpack/test/dispatch/host_authorization_test.rb6
-rw-r--r--actionpack/test/dispatch/mount_test.rb6
-rw-r--r--actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb27
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb9
-rw-r--r--actionpack/test/dispatch/system_testing/driver_test.rb69
-rw-r--r--actionpack/test/dispatch/system_testing/screenshot_helper_test.rb1
-rw-r--r--actionpack/test/dispatch/system_testing/system_test_case_test.rb1
-rw-r--r--actionpack/test/journey/path/pattern_test.rb22
-rw-r--r--actiontext/app/helpers/action_text/tag_helper.rb4
-rw-r--r--actiontext/app/models/action_text/rich_text.rb6
-rw-r--r--actiontext/app/views/active_storage/blobs/_blob.html.erb2
-rw-r--r--actiontext/db/migrate/20180528164100_create_action_text_tables.rb (renamed from actiontext/db/migrate/201805281641_create_action_text_tables.rb)5
-rw-r--r--actiontext/lib/templates/installer.rb2
-rw-r--r--actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb (renamed from actiontext/test/dummy/db/migrate/2018052816_create_action_text_tables.rb)5
-rw-r--r--actiontext/test/dummy/db/schema.rb15
-rw-r--r--actiontext/test/unit/model_test.rb9
-rw-r--r--actionview/CHANGELOG.md22
-rw-r--r--actionview/Rakefile27
-rw-r--r--actionview/lib/action_view.rb2
-rw-r--r--actionview/lib/action_view/base.rb79
-rw-r--r--actionview/lib/action_view/file_template.rb33
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb10
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb2
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb2
-rw-r--r--actionview/lib/action_view/lookup_context.rb73
-rw-r--r--actionview/lib/action_view/railtie.rb11
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb4
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb97
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb18
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb4
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb20
-rw-r--r--actionview/lib/action_view/rendering.rb43
-rw-r--r--actionview/lib/action_view/template.rb49
-rw-r--r--actionview/lib/action_view/template/handlers.rb28
-rw-r--r--actionview/lib/action_view/template/handlers/builder.rb4
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb10
-rw-r--r--actionview/lib/action_view/template/handlers/erb/erubi.rb2
-rw-r--r--actionview/lib/action_view/template/handlers/html.rb2
-rw-r--r--actionview/lib/action_view/template/handlers/raw.rb4
-rw-r--r--actionview/lib/action_view/template/resolver.rb3
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb6
-rw-r--r--actionview/lib/action_view/view_paths.rb26
-rw-r--r--actionview/test/abstract_unit.rb8
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb4
-rw-r--r--actionview/test/actionpack/controller/render_test.rb10
-rw-r--r--actionview/test/actionpack/controller/view_paths_test.rb4
-rw-r--r--actionview/test/activerecord/multifetch_cache_test.rb6
-rw-r--r--actionview/test/template/capture_helper_test.rb2
-rw-r--r--actionview/test/template/compiled_templates_test.rb17
-rw-r--r--actionview/test/template/dependency_tracker_test.rb4
-rw-r--r--actionview/test/template/form_collections_helper_test.rb24
-rw-r--r--actionview/test/template/log_subscriber_test.rb7
-rw-r--r--actionview/test/template/lookup_context_test.rb47
-rw-r--r--actionview/test/template/render_test.rb47
-rw-r--r--actionview/test/template/resolver_cache_test.rb1
-rw-r--r--actionview/test/template/streaming_render_test.rb5
-rw-r--r--actionview/test/template/template_test.rb6
-rw-r--r--actionview/test/template/test_case_test.rb7
-rw-r--r--actionview/test/template/translation_helper_test.rb5
-rw-r--r--actionview/test/ujs/server.rb1
-rw-r--r--activejob/lib/active_job/enqueuing.rb2
-rw-r--r--activejob/lib/active_job/queue_name.rb20
-rw-r--r--activejob/test/cases/test_helper_test.rb2
-rw-r--r--activejob/test/support/integration/adapters/backburner.rb1
-rw-r--r--activejob/test/support/integration/adapters/resque.rb2
-rw-r--r--activejob/test/support/integration/adapters/sneakers.rb2
-rw-r--r--activemodel/lib/active_model/validations/length.rb2
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb2
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb2
-rw-r--r--activerecord/CHANGELOG.md54
-rw-r--r--activerecord/Rakefile13
-rw-r--r--activerecord/lib/active_record.rb7
-rw-r--r--activerecord/lib/active_record/associations/association.rb17
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb14
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb53
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb24
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb76
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb82
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb10
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb187
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb13
-rw-r--r--activerecord/lib/active_record/connection_handling.rb13
-rw-r--r--activerecord/lib/active_record/core.rb14
-rw-r--r--activerecord/lib/active_record/database_configurations.rb4
-rw-r--r--activerecord/lib/active_record/database_configurations/url_config.rb13
-rw-r--r--activerecord/lib/active_record/middleware/database_selector.rb75
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver.rb90
-rw-r--r--activerecord/lib/active_record/middleware/database_selector/resolver/session.rb45
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb121
-rw-r--r--activerecord/lib/active_record/persistence.rb6
-rw-r--r--activerecord/lib/active_record/querying.rb3
-rw-r--r--activerecord/lib/active_record/railtie.rb9
-rw-r--r--activerecord/lib/active_record/railties/collection_cache_association_loading.rb6
-rw-r--r--activerecord/lib/active_record/reflection.rb20
-rw-r--r--activerecord/lib/active_record/relation.rb24
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb6
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb2
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb12
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb22
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb2
-rw-r--r--activerecord/lib/active_record/scoping.rb19
-rw-r--r--activerecord/lib/active_record/scoping/default.rb9
-rw-r--r--activerecord/lib/active_record/test_fixtures.rb4
-rw-r--r--activerecord/lib/arel/nodes/and.rb2
-rw-r--r--activerecord/lib/arel/nodes/case.rb4
-rw-r--r--activerecord/test/cases/adapter_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb13
-rw-r--r--activerecord/test/cases/ar_schema_test.rb78
-rw-r--r--activerecord/test/cases/arel/nodes/and_test.rb9
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb12
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb6
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb58
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb7
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb12
-rw-r--r--activerecord/test/cases/base_test.rb7
-rw-r--r--activerecord/test/cases/cache_key_test.rb6
-rw-r--r--activerecord/test/cases/calculations_test.rb5
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb5
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb48
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb8
-rw-r--r--activerecord/test/cases/database_selector_test.rb166
-rw-r--r--activerecord/test/cases/finder_test.rb10
-rw-r--r--activerecord/test/cases/fixtures_test.rb2
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb6
-rw-r--r--activerecord/test/cases/migration/columns_test.rb11
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb100
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb158
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb11
-rw-r--r--activerecord/test/cases/migration_test.rb12
-rw-r--r--activerecord/test/cases/query_cache_test.rb38
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb10
-rw-r--r--activerecord/test/cases/relations_test.rb43
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb24
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb10
-rw-r--r--activerecord/test/cases/statement_cache_test.rb6
-rw-r--r--activerecord/test/cases/unconnected_test.rb8
-rw-r--r--activerecord/test/config.example.yml6
-rw-r--r--activerecord/test/models/bird.rb5
-rw-r--r--activerecord/test/models/company.rb2
-rw-r--r--activerecord/test/models/contract.rb8
-rw-r--r--activerecord/test/models/post.rb1
-rw-r--r--activerecord/test/models/subscription.rb2
-rw-r--r--activerecord/test/models/topic.rb15
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb8
-rw-r--r--activerecord/test/schema/schema.rb13
-rw-r--r--activerecord/test/support/connection.rb1
-rw-r--r--activestorage/CHANGELOG.md6
-rw-r--r--activestorage/README.md2
-rw-r--r--activestorage/app/models/active_storage/blob/representable.rb10
-rw-r--r--activestorage/app/models/active_storage/preview.rb2
-rw-r--r--activestorage/app/models/active_storage/variant.rb8
-rw-r--r--activestorage/app/models/active_storage/variation.rb2
-rw-r--r--activestorage/lib/active_storage/analyzer/video_analyzer.rb2
-rw-r--r--activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb2
-rw-r--r--activestorage/lib/active_storage/service/azure_storage_service.rb4
-rw-r--r--activestorage/lib/active_storage/service/s3_service.rb4
-rw-r--r--activestorage/test/service/s3_service_test.rb18
-rw-r--r--activesupport/CHANGELOG.md22
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb46
-rw-r--r--activesupport/lib/active_support/core_ext/kernel.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/agnostics.rb13
-rw-r--r--activesupport/lib/active_support/current_attributes.rb6
-rw-r--r--activesupport/lib/active_support/dependencies/zeitwerk_integration.rb73
-rw-r--r--activesupport/lib/active_support/encrypted_file.rb3
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb13
-rw-r--r--activesupport/lib/active_support/notifications.rb9
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb69
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb2
-rw-r--r--activesupport/test/cache/stores/redis_cache_store_test.rb2
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb27
-rw-r--r--activesupport/test/current_attributes_test.rb37
-rw-r--r--activesupport/test/dependencies_test.rb2
-rw-r--r--activesupport/test/hash_with_indifferent_access_test.rb10
-rw-r--r--activesupport/test/notifications/evented_notification_test.rb33
-rw-r--r--activesupport/test/notifications_test.rb21
-rw-r--r--activesupport/test/safe_buffer_test.rb2
-rw-r--r--activesupport/test/share_lock_test.rb2
-rw-r--r--activesupport/test/time_travel_test.rb2
-rw-r--r--activesupport/test/time_zone_test.rb2
-rw-r--r--ci/qunit-selenium-runner.rb18
-rw-r--r--guides/Rakefile11
-rw-r--r--guides/assets/images/getting_started/template_is_missing_articles_new.pngbin472167 -> 26796 bytes
-rw-r--r--guides/bug_report_templates/active_record_gem.rb2
-rw-r--r--guides/bug_report_templates/active_record_migrations_gem.rb2
-rw-r--r--guides/rails_guides/generator.rb4
-rw-r--r--guides/source/action_controller_overview.md2
-rw-r--r--guides/source/active_record_migrations.md3
-rw-r--r--guides/source/active_storage_overview.md2
-rw-r--r--guides/source/active_support_core_extensions.md2
-rw-r--r--guides/source/api_app.md2
-rw-r--r--guides/source/asset_pipeline.md2
-rw-r--r--guides/source/configuring.md9
-rw-r--r--guides/source/engines.md2
-rw-r--r--guides/source/getting_started.md23
-rw-r--r--guides/source/routing.md2
-rw-r--r--guides/source/testing.md33
-rw-r--r--package.json5
-rw-r--r--railties/.gitignore1
-rw-r--r--railties/CHANGELOG.md14
-rw-r--r--railties/Rakefile21
-rw-r--r--railties/lib/minitest/rails_plugin.rb2
-rw-r--r--railties/lib/rails.rb20
-rw-r--r--railties/lib/rails/app_loader.rb2
-rw-r--r--railties/lib/rails/application.rb38
-rw-r--r--railties/lib/rails/application/configuration.rb15
-rw-r--r--railties/lib/rails/application/finisher.rb8
-rw-r--r--railties/lib/rails/command/actions.rb10
-rw-r--r--railties/lib/rails/command/base.rb14
-rw-r--r--railties/lib/rails/command/behavior.rb5
-rw-r--r--railties/lib/rails/commands/credentials/USAGE2
-rw-r--r--railties/lib/rails/commands/credentials/credentials_command.rb4
-rw-r--r--railties/lib/rails/engine.rb25
-rw-r--r--railties/lib/rails/generators/actions.rb6
-rw-r--r--railties/lib/rails/generators/database.rb1
-rw-r--r--railties/lib/rails/generators/migration.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt21
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt3
-rw-r--r--railties/test/application/configuration_test.rb149
-rw-r--r--railties/test/application/mailer_previews_test.rb3
-rw-r--r--railties/test/application/multiple_applications_test.rb24
-rw-r--r--railties/test/application/rake/dbs_test.rb2
-rw-r--r--railties/test/application/rake_test.rb4
-rw-r--r--railties/test/application/server_test.rb4
-rw-r--r--railties/test/application/test_runner_test.rb5
-rw-r--r--railties/test/application/url_generation_test.rb1
-rw-r--r--railties/test/application/zeitwerk_integration_test.rb164
-rw-r--r--railties/test/command/base_test.rb2
-rw-r--r--railties/test/commands/credentials_test.rb8
-rw-r--r--railties/test/commands/dbconsole_test.rb4
-rw-r--r--railties/test/generators/actions_test.rb6
-rw-r--r--railties/test/generators/app_generator_test.rb13
-rw-r--r--railties/test/generators/generators_test_helper.rb6
-rw-r--r--railties/test/generators/mailer_generator_test.rb2
-rw-r--r--railties/test/generators/model_generator_test.rb2
-rw-r--r--railties/test/generators/plugin_generator_test.rb2
-rw-r--r--railties/test/isolation/abstract_unit.rb30
-rw-r--r--railties/test/isolation/assets/config/webpack/development.js3
-rw-r--r--railties/test/isolation/assets/config/webpack/production.js3
-rw-r--r--railties/test/isolation/assets/config/webpack/test.js3
-rw-r--r--railties/test/isolation/assets/config/webpacker.yml8
-rw-r--r--railties/test/isolation/assets/package.json11
-rw-r--r--railties/test/railties/engine_test.rb58
-rw-r--r--railties/test/railties/mounted_engine_test.rb6
-rw-r--r--tasks/release.rb5
314 files changed, 3916 insertions, 1292 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index dce1a30d9f..618817daca 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -179,6 +179,9 @@ Layout/TrailingWhitespace:
Style/UnneededPercentQ:
Enabled: true
+Lint/ErbNewArguments:
+ Enabled: true
+
# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg.
Lint/RequireParentheses:
Enabled: true
diff --git a/.travis.yml b/.travis.yml
index 8317b58c52..ba1263ad2e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,3 +1,4 @@
+dist: xenial
language: ruby
sudo: false
@@ -12,24 +13,20 @@ cache:
services:
- memcached
- redis-server
+ - mysql
addons:
postgresql: 10
chrome: stable
apt:
sources:
- - sourceline: "ppa:mc3man/trusty-media"
+ - sourceline: "ppa:jonathonf/ffmpeg-3"
- sourceline: "ppa:ubuntuhandbook1/apps"
- - mysql-5.7-trusty
packages:
- ffmpeg
- mupdf
- mupdf-tools
- poppler-utils
- - mysql-server
- - mysql-client
- - postgresql-10
- - postgresql-client-10
bundler_args: --jobs 3 --retry 3
before_install:
@@ -43,12 +40,15 @@ before_install:
- "[[ $GEM != 'actionview:ujs' ]] || (cd actionview && npm install)"
- "[[ $GEM != 'railties' ]] || (curl -o- -L https://yarnpkg.com/install.sh | bash)"
- "[[ $GEM != 'railties' ]] || export PATH=$HOME/.yarn/bin:$PATH"
+ - "[[ $GEM != 'activerecord:postgresql' ]] || sudo mount -o remount,size=50% /var/ramfs"
before_script:
# Set Sauce Labs username and access key. Obfuscated, purposefully not encrypted.
# Decodes to e.g. `export VARIABLE=VALUE`
- $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX0FDQ0VTU19LRVk9YTAzNTM0M2YtZTkyMi00MGIzLWFhM2MtMDZiM2VhNjM1YzQ4")
- $(base64 --decode <<< "ZXhwb3J0IFNBVUNFX1VTRVJOQU1FPXJ1YnlvbnJhaWxz")
+ - "[[ $GEM != 'railties' ]] || (cd actionview && yarn build)"
+ - "[[ $GEM != 'railties' ]] || (cd railties/test/isolation/assets && yarn install)"
script: 'ci/travis.rb'
@@ -56,10 +56,13 @@ env:
global:
- "JRUBY_OPTS='--dev -J-Xmx1024M'"
matrix:
+ - "GEM=railties"
- "GEM=actionpack,actioncable"
- "GEM=actionmailer,activemodel,activesupport,actionview,activejob,activestorage,actionmailbox,actiontext"
- "GEM=activesupport PRESERVE_TIMEZONES=1"
- "GEM=activerecord:sqlite3"
+ - "GEM=activerecord:postgresql"
+ - "GEM=activerecord:mysql2"
- "GEM=guides"
- "GEM=actioncable:integration"
@@ -71,126 +74,72 @@ rvm:
matrix:
include:
- rvm: 2.5.3
- env: "GEM=railties"
- sudo: required
- before_install:
- - "rm ${BUNDLE_GEMFILE}.lock"
- - "travis_retry gem update --system"
- - "travis_retry gem install bundler"
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
- - rvm: 2.6.0
- env: "GEM=railties"
- sudo: required
- before_install:
- - "rm ${BUNDLE_GEMFILE}.lock"
- - "travis_retry gem update --system"
- - "travis_retry gem install bundler"
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
- - rvm: ruby-head
- env: "GEM=railties"
- sudo: required
- before_install:
- - "rm ${BUNDLE_GEMFILE}.lock"
- - "travis_retry gem update --system"
- - "travis_retry gem install bundler"
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
- - rvm: 2.5.3
env: "GEM=actionview:ujs"
- rvm: 2.5.3
sudo: required
env: "GEM=activejob:integration"
+ addons:
+ postgresql: 10
+ apt:
+ packages:
+ - rabbitmq-server
services:
- memcached
- redis-server
- - rabbitmq
+ - rabbitmq-server
before_install:
- - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
- "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
- "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
- rvm: 2.6.0
sudo: required
env: "GEM=activejob:integration"
+ addons:
+ postgresql: 10
+ apt:
+ packages:
+ - rabbitmq-server
services:
- memcached
- redis-server
- - rabbitmq
+ - rabbitmq-server
before_install:
- - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
- "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
- "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
- rvm: ruby-head
sudo: required
env: "GEM=activejob:integration"
+ addons:
+ postgresql: 10
+ apt:
+ packages:
+ - rabbitmq-server
services:
- memcached
- redis-server
- - rabbitmq
+ - rabbitmq-server
before_install:
- - sudo sed -i -e '/local.*peer/s/postgres/all/' -e 's/peer\|md5/trust/g' /etc/postgresql/*/main/pg_hba.conf
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
+ - "rm ${BUNDLE_GEMFILE}.lock"
+ - "travis_retry gem update --system"
+ - "travis_retry gem install bundler"
- "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/beanstalkd/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)"
- "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd"
- rvm: 2.5.3
- env: "GEM=activerecord:mysql2"
- sudo: required
- before_install:
- - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\""
- - "sudo mysql_upgrade"
- - "sudo service mysql restart"
- - rvm: 2.6.0
- env: "GEM=activerecord:mysql2"
- sudo: required
- before_install:
- - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\""
- - "sudo mysql_upgrade"
- - "sudo service mysql restart"
- - rvm: ruby-head
- env: "GEM=activerecord:mysql2"
- sudo: required
+ env:
+ - "GEM=activerecord:mysql2 MYSQL=mariadb"
before_install:
- - "sudo mysql -e \"use mysql; update user set authentication_string='' where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;\""
- "sudo mysql_upgrade"
- "sudo service mysql restart"
- - rvm: 2.5.3
- env:
- - "GEM=activerecord:mysql2 MYSQL=mariadb"
addons:
mariadb: 10.3
- rvm: 2.5.3
env:
- "GEM=activerecord:sqlite3_mem"
- - rvm: 2.5.3
- env: "GEM=activerecord:postgresql"
- sudo: required
- before_install:
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
- - rvm: 2.6.0
- env: "GEM=activerecord:postgresql"
- sudo: required
- before_install:
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
- - rvm: ruby-head
- env: "GEM=activerecord:postgresql"
- sudo: required
- before_install:
- - "sudo sed -i 's/port = 5433/port = 5432/' /etc/postgresql/10/main/postgresql.conf"
- - "sudo service postgresql restart 10"
- rvm: jruby-head
- jdk: oraclejdk8
+ jdk: oraclejdk11
env:
- "GEM=actionpack"
- rvm: jruby-head
- jdk: oraclejdk8
+ jdk: oraclejdk11
env:
- "GEM=actionmailer,activemodel,activejob"
allow_failures:
diff --git a/Gemfile b/Gemfile
index fe5f1b2f3c..fda8c86cae 100644
--- a/Gemfile
+++ b/Gemfile
@@ -10,6 +10,7 @@ gemspec
gem "rake", ">= 11.1"
gem "capybara", ">= 2.15"
+gem "selenium-webdriver", ">= 3.5.0", "< 3.13.0"
gem "rack-cache", "~> 1.2"
gem "sass-rails"
@@ -43,7 +44,9 @@ gem "libxml-ruby", platforms: :ruby
gem "connection_pool", require: false
# for railties app_generator_test
-gem "bootsnap", ">= 1.1.0", require: false
+gem "bootsnap", ">= 1.4.0", require: false
+
+gem "zeitwerk", ">= 1.0.0" if RUBY_ENGINE == "ruby"
# Active Job
group :job do
@@ -52,7 +55,7 @@ group :job do
gem "sidekiq", require: false
gem "sucker_punch", require: false
gem "delayed_job", require: false
- gem "queue_classic", github: "rafaelfranca/queue_classic", branch: "update-pg", require: false, platforms: :ruby
+ gem "queue_classic", github: "QueueClassic/queue_classic", require: false, platforms: :ruby
gem "sneakers", require: false
gem "que", require: false
gem "backburner", require: false
@@ -117,7 +120,7 @@ platforms :ruby, :mswin, :mswin64, :mingw, :x64_mingw do
gem "racc", ">=1.4.6", require: false
# Active Record.
- gem "sqlite3", "~> 1.3.6"
+ gem "sqlite3", "~> 1.3", ">= 1.3.6"
group :db do
gem "pg", ">= 0.18.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index 70a29f501d..fa8094bf23 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,4 +1,11 @@
GIT
+ remote: https://github.com/QueueClassic/queue_classic.git
+ revision: 4cdc9b8e804badf7dea7078dd81092972d292c14
+ specs:
+ queue_classic (3.2.0.RC1)
+ pg (>= 0.17, < 2.0)
+
+GIT
remote: https://github.com/matthewd/websocket-client-simple.git
revision: e161305f1a466b9398d86df3b1731b03362da91b
branch: close-race
@@ -7,14 +14,6 @@ GIT
event_emitter
websocket
-GIT
- remote: https://github.com/rafaelfranca/queue_classic.git
- revision: dee64b361355d56700ad7aa3b151bf653a617526
- branch: update-pg
- specs:
- queue_classic (3.2.0.RC1)
- pg (>= 0.17, < 2.0)
-
PATH
remote: .
specs:
@@ -167,13 +166,13 @@ GEM
childprocess
faraday
selenium-webdriver
- bootsnap (1.3.2)
+ bootsnap (1.4.0)
msgpack (~> 1.0)
- bootsnap (1.3.2-java)
+ bootsnap (1.4.0-java)
msgpack (~> 1.0)
builder (3.2.3)
- bunny (2.9.2)
- amq-protocol (~> 2.3.0)
+ bunny (2.13.0)
+ amq-protocol (~> 2.3, >= 2.3.0)
byebug (10.0.2)
capybara (3.10.1)
addressable
@@ -192,7 +191,7 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.12.2)
- concurrent-ruby (1.1.3)
+ concurrent-ruby (1.1.4)
connection_pool (2.2.2)
cookiejar (0.3.3)
crack (0.4.3)
@@ -239,10 +238,10 @@ GEM
faye-websocket (0.10.7)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
- ffi (1.9.25)
- ffi (1.9.25-java)
- ffi (1.9.25-x64-mingw32)
- ffi (1.9.25-x86-mingw32)
+ ffi (1.10.0)
+ ffi (1.10.0-java)
+ ffi (1.10.0-x64-mingw32)
+ ffi (1.10.0-x86-mingw32)
fugit (1.1.6)
et-orbi (~> 1.1, >= 1.1.6)
raabro (~> 1.1)
@@ -325,10 +324,10 @@ GEM
minitest-server (1.0.5)
minitest (~> 5.0)
mono_logger (1.1.0)
- msgpack (1.2.4)
- msgpack (1.2.4-java)
- msgpack (1.2.4-x64-mingw32)
- msgpack (1.2.4-x86-mingw32)
+ msgpack (1.2.6)
+ msgpack (1.2.6-java)
+ msgpack (1.2.6-x64-mingw32)
+ msgpack (1.2.6-x86-mingw32)
multi_json (1.13.1)
multipart-post (2.0.0)
mustache (1.1.0)
@@ -337,6 +336,7 @@ GEM
mysql2 (0.5.2-x64-mingw32)
mysql2 (0.5.2-x86-mingw32)
nio4r (2.3.1)
+ nio4r (2.3.1-java)
nokogiri (1.9.1)
mini_portile2 (~> 2.4.0)
nokogiri (1.9.1-java)
@@ -433,9 +433,9 @@ GEM
tilt (>= 1.1, < 3)
sdoc (1.0.0)
rdoc (>= 5.0)
- selenium-webdriver (3.141.0)
+ selenium-webdriver (3.12.0)
childprocess (~> 0.5)
- rubyzip (~> 1.2, >= 1.2.2)
+ rubyzip (~> 1.2)
sequel (5.14.0)
serverengine (2.0.7)
sigdump (~> 0.2.2)
@@ -454,9 +454,10 @@ GEM
rack (~> 2.0)
rack-protection (= 2.0.4)
tilt (~> 2.0)
- sneakers (2.7.0)
- bunny (~> 2.9.2)
+ sneakers (2.11.0)
+ bunny (~> 2.12)
concurrent-ruby (~> 1.0)
+ rake
serverengine (~> 2.0.5)
thor
sprockets (3.7.2)
@@ -510,9 +511,12 @@ GEM
websocket (1.2.8)
websocket-driver (0.7.0)
websocket-extensions (>= 0.1.0)
+ websocket-driver (0.7.0-java)
+ websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.3)
xpath (3.2.0)
nokogiri (~> 1.8)
+ zeitwerk (1.0.0)
PLATFORMS
java
@@ -532,7 +536,7 @@ DEPENDENCIES
benchmark-ips
blade
blade-sauce_labs_plugin
- bootsnap (>= 1.1.0)
+ bootsnap (>= 1.4.0)
byebug
capybara (>= 2.15)
chromedriver-helper
@@ -569,11 +573,12 @@ DEPENDENCIES
rubocop (>= 0.47)
sass-rails
sdoc (~> 1.0)
+ selenium-webdriver (>= 3.5.0, < 3.13.0)
sequel
sidekiq
sneakers
sprockets-export
- sqlite3 (~> 1.3.6)
+ sqlite3 (~> 1.3, >= 1.3.6)
stackprof
sucker_punch
turbolinks (~> 5)
@@ -584,6 +589,7 @@ DEPENDENCIES
webmock
webpacker (>= 4.0.0.rc.3)
websocket-client-simple!
+ zeitwerk (>= 1.0.0)
BUNDLED WITH
- 1.17.2
+ 1.17.3
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index 4109dd6138..3ce35edbb8 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,5 +1,66 @@
+* Allow passing custom configuration to `ActionCable::Server::Base`.
+
+ You can now create a standalone Action Cable server with a custom configuration
+ (e.g. to run it in isolation from the default one):
+
+ ```ruby
+ config = ActionCable::Server::Configuration.new
+ config.cable = { adapter: "redis", channel_prefix: "custom_" }
+
+ CUSTOM_CABLE = ActionCable::Server::Base.new(config: config)
+ ```
+
+ Then you can mount it in the `routes.rb` file:
+
+ ```ruby
+ Rails.application.routes.draw do
+ mount CUSTOM_CABLE => "/custom_cable"
+ # ...
+ end
+ ```
+
+ *Vladimir Dementyev*
+
+* Add `:action_cable_connection` and `:action_cable_channel` load hooks.
+
+ You can use them to extend `ActionCable::Connection::Base` and `ActionCable::Channel::Base`
+ functionality:
+
+ ```ruby
+ ActiveSupport.on_load(:action_cable_channel) do
+ # do something in the context of ActionCable::Channel::Base
+ end
+ ```
+
+ *Vladimir Dementyev*
+
+* Add `Channel::Base#broadcast_to`.
+
+ You can now call `broadcast_to` within a channel action, which equals to
+ the `self.class.broadcast_to`.
+
+ *Vladimir Dementyev*
+
+* Make `Channel::Base.broadcasting_for` a public API.
+
+ You can use `.broadcasting_for` to generate a unique stream identifier within
+ a channel for the specified target (e.g. Active Record model):
+
+ ```ruby
+ ChatChannel.broadcasting_for(model) # => "chat:<model.to_gid_param>"
+ ```
+
+ *Vladimir Dementyev*
+
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
+* [Rename npm package](https://github.com/rails/rails/pull/34905) from
+ [`actioncable`](https://www.npmjs.com/package/actioncable) to
+ [`@rails/actioncable`](https://www.npmjs.com/package/@rails/actioncable).
+
+ *Javan Makhmali*
+
* Merge [`action-cable-testing`](https://github.com/palkan/action-cable-testing) to Rails.
*Vladimir Dementyev*
diff --git a/actioncable/Rakefile b/actioncable/Rakefile
index 35de50f05a..121037844d 100644
--- a/actioncable/Rakefile
+++ b/actioncable/Rakefile
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "base64"
require "rake/testtask"
require "pathname"
require "open3"
@@ -25,7 +26,8 @@ namespace :test do
end
task :integration do
- system("yarn test") || raise("Failures")
+ system(Hash[*Base64.decode64(ENV.fetch("ENCODED", "")).split(/[ =]/)], "yarn", "test")
+ exit($?.exitstatus) unless $?.success?
end
end
diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb
index ad0d3685cd..af061c843a 100644
--- a/actioncable/lib/action_cable/channel/base.rb
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -307,3 +307,5 @@ module ActionCable
end
end
end
+
+ActiveSupport.run_load_hooks(:action_cable_channel, ActionCable::Channel::Base)
diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb
index 9a96720f4a..9f702e425e 100644
--- a/actioncable/lib/action_cable/channel/broadcasting.rb
+++ b/actioncable/lib/action_cable/channel/broadcasting.rb
@@ -7,22 +7,32 @@ module ActionCable
module Broadcasting
extend ActiveSupport::Concern
- delegate :broadcasting_for, to: :class
+ delegate :broadcasting_for, :broadcast_to, to: :class
module ClassMethods
# Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
def broadcast_to(model, message)
- ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message)
+ ActionCable.server.broadcast(broadcasting_for(model), message)
end
- def broadcasting_for(model) #:nodoc:
+ # Returns a unique broadcasting identifier for this <tt>model</tt> in this channel:
+ #
+ # CommentsChannel.broadcasting_for("all") # => "comments:all"
+ #
+ # You can pass any object as a target (e.g. Active Record model), and it
+ # would be serialized into a string under the hood.
+ def broadcasting_for(model)
+ serialize_broadcasting([ channel_name, model ])
+ end
+
+ def serialize_broadcasting(object) #:nodoc:
case
- when model.is_a?(Array)
- model.map { |m| broadcasting_for(m) }.join(":")
- when model.respond_to?(:to_gid_param)
- model.to_gid_param
+ when object.is_a?(Array)
+ object.map { |m| serialize_broadcasting(m) }.join(":")
+ when object.respond_to?(:to_gid_param)
+ object.to_gid_param
else
- model.to_param
+ object.to_param
end
end
end
diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb
index 81c2c38064..7e1ed3c850 100644
--- a/actioncable/lib/action_cable/channel/streams.rb
+++ b/actioncable/lib/action_cable/channel/streams.rb
@@ -99,7 +99,7 @@ module ActionCable
# Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
# Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
def stream_for(model, callback = nil, coder: nil, &block)
- stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder)
+ stream_from(broadcasting_for(model), callback || block, coder: coder)
end
# Unsubscribes all streams associated with this channel from the pubsub queue.
diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb
index c4cf0ac0e7..b05d51a61a 100644
--- a/actioncable/lib/action_cable/channel/test_case.rb
+++ b/actioncable/lib/action_cable/channel/test_case.rb
@@ -143,7 +143,7 @@ module ActionCable
# You need to set up your connection manually to provide values for the identifiers.
# To do this just use:
#
- # stub_connection(user: users[:john])
+ # stub_connection(user: users(:john))
#
# == Testing broadcasting
#
@@ -157,9 +157,9 @@ module ActionCable
# end
#
# def test_speak
- # subscribe room_id: rooms[:chat].id
+ # subscribe room_id: rooms(:chat).id
#
- # assert_broadcasts_on(rooms[:chat], text: "Hello, Rails!") do
+ # assert_broadcasts_on(rooms(:chat), text: "Hello, Rails!") do
# perform :speak, message: "Hello, Rails!"
# end
# end
@@ -300,9 +300,7 @@ module ActionCable
def broadcasting_for(stream_or_object)
return stream_or_object if stream_or_object.is_a?(String)
- self.class.channel_class.broadcasting_for(
- [self.class.channel_class.channel_name, stream_or_object]
- )
+ self.class.channel_class.broadcasting_for(stream_or_object)
end
end
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index 0044afad98..c469f7066c 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -260,3 +260,5 @@ module ActionCable
end
end
end
+
+ActiveSupport.run_load_hooks(:action_cable_connection, ActionCable::Connection::Base)
diff --git a/actioncable/lib/action_cable/connection/test_case.rb b/actioncable/lib/action_cable/connection/test_case.rb
index 26a183d1ec..8d25a55c8a 100644
--- a/actioncable/lib/action_cable/connection/test_case.rb
+++ b/actioncable/lib/action_cable/connection/test_case.rb
@@ -42,8 +42,6 @@ module ActionCable
class TestRequest < ActionDispatch::TestRequest
attr_accessor :session, :cookie_jar
-
- attr_writer :cookie_jar
end
module TestConnection
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
index 2b9e1cba3b..98b3743175 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -12,14 +12,17 @@ module ActionCable
include ActionCable::Server::Broadcasting
include ActionCable::Server::Connections
- cattr_accessor :config, instance_accessor: true, default: ActionCable::Server::Configuration.new
+ cattr_accessor :config, instance_accessor: false, default: ActionCable::Server::Configuration.new
+
+ attr_reader :config
def self.logger; config.logger; end
delegate :logger, to: :config
attr_reader :mutex
- def initialize
+ def initialize(config: self.class.config)
+ @config = config
@mutex = Monitor.new
@remote_connections = @event_loop = @worker_pool = @pubsub = nil
end
diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt
index 33baaa5a22..ddf6b2d79b 100644
--- a/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/channel.js.tt
@@ -1,15 +1,15 @@
import consumer from "./consumer"
consumer.subscriptions.create("<%= class_name %>Channel", {
- connected: function() {
+ connected() {
// Called when the subscription is ready for use on the server
},
- disconnected: function() {
+ disconnected() {
// Called when the subscription has been terminated by the server
},
- received: function(data) {
+ received(data) {
// Called when there's incoming data on the websocket for this channel
}<%= actions.any? ? ",\n" : '' %>
<% actions.each do |action| -%>
diff --git a/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
index eec7e54b8a..0eceb59b18 100644
--- a/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
+++ b/actioncable/lib/rails/generators/channel/templates/javascript/consumer.js.tt
@@ -1,6 +1,6 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
-import ActionCable from "@rails/actioncable"
+import { createConsumer } from "@rails/actioncable"
-export default ActionCable.createConsumer()
+export default createConsumer()
diff --git a/actioncable/test/channel/broadcasting_test.rb b/actioncable/test/channel/broadcasting_test.rb
index 2cbfabc1d0..fb501a1bc2 100644
--- a/actioncable/test/channel/broadcasting_test.rb
+++ b/actioncable/test/channel/broadcasting_test.rb
@@ -26,14 +26,23 @@ class ActionCable::Channel::BroadcastingTest < ActionCable::TestCase
end
test "broadcasting_for with an object" do
- assert_equal "Room#1-Campfire", ChatChannel.broadcasting_for(Room.new(1))
+ assert_equal(
+ "action_cable:channel:broadcasting_test:chat:Room#1-Campfire",
+ ChatChannel.broadcasting_for(Room.new(1))
+ )
end
test "broadcasting_for with an array" do
- assert_equal "Room#1-Campfire:Room#2-Campfire", ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ])
+ assert_equal(
+ "action_cable:channel:broadcasting_test:chat:Room#1-Campfire:Room#2-Campfire",
+ ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ])
+ )
end
test "broadcasting_for with a string" do
- assert_equal "hello", ChatChannel.broadcasting_for("hello")
+ assert_equal(
+ "action_cable:channel:broadcasting_test:chat:hello",
+ ChatChannel.broadcasting_for("hello")
+ )
end
end
diff --git a/actioncable/test/channel/test_case_test.rb b/actioncable/test/channel/test_case_test.rb
index 9c360d5dc3..a166c41e11 100644
--- a/actioncable/test/channel/test_case_test.rb
+++ b/actioncable/test/channel/test_case_test.rb
@@ -180,7 +180,7 @@ class BroadcastsTestChannel < ActionCable::Channel::Base
def broadcast_to_user(data)
user = User.new user_id
- self.class.broadcast_to user, text: data["message"]
+ broadcast_to user, text: data["message"]
end
end
diff --git a/actioncable/test/subscription_adapter/channel_prefix.rb b/actioncable/test/subscription_adapter/channel_prefix.rb
index 3071facd9d..475e6cfd3a 100644
--- a/actioncable/test/subscription_adapter/channel_prefix.rb
+++ b/actioncable/test/subscription_adapter/channel_prefix.rb
@@ -2,17 +2,9 @@
require "test_helper"
-class ActionCable::Server::WithIndependentConfig < ActionCable::Server::Base
- # ActionCable::Server::Base defines config as a class variable.
- # Need config to be an instance variable here as we're testing 2 separate configs
- def config
- @config ||= ActionCable::Server::Configuration.new
- end
-end
-
module ChannelPrefixTest
def test_channel_prefix
- server2 = ActionCable::Server::WithIndependentConfig.new
+ server2 = ActionCable::Server::Base.new(config: ActionCable::Server::Configuration.new)
server2.config.cable = alt_cable_config
server2.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
diff --git a/actioncable/test/subscription_adapter/redis_test.rb b/actioncable/test/subscription_adapter/redis_test.rb
index ac2d8ef724..35840a4036 100644
--- a/actioncable/test/subscription_adapter/redis_test.rb
+++ b/actioncable/test/subscription_adapter/redis_test.rb
@@ -11,7 +11,11 @@ class RedisAdapterTest < ActionCable::TestCase
include ChannelPrefixTest
def cable_config
- { adapter: "redis", driver: "ruby" }
+ { adapter: "redis", driver: "ruby" }.tap do |x|
+ if host = URI(ENV["REDIS_URL"] || "").hostname
+ x[:host] = host
+ end
+ end
end
end
@@ -25,7 +29,7 @@ class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest
def cable_config
alt_cable_config = super.dup
alt_cable_config.delete(:url)
- alt_cable_config.merge(host: "127.0.0.1", port: 6379, db: 12)
+ alt_cable_config.merge(host: URI(ENV["REDIS_URL"] || "").hostname || "127.0.0.1", port: 6379, db: 12)
end
end
diff --git a/actionmailbox/CHANGELOG.md b/actionmailbox/CHANGELOG.md
index d92367dc17..3e92283c12 100644
--- a/actionmailbox/CHANGELOG.md
+++ b/actionmailbox/CHANGELOG.md
@@ -1,3 +1,9 @@
+* Allow skipping incineration of processed emails.
+
+ This can be done by setting `config.action_mailbox.incinerate` to `false`.
+
+ *Pratik Naik*
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
* Added to Rails.
diff --git a/actionmailbox/app/jobs/action_mailbox/incineration_job.rb b/actionmailbox/app/jobs/action_mailbox/incineration_job.rb
index 1579a3c7c8..351bd67c69 100644
--- a/actionmailbox/app/jobs/action_mailbox/incineration_job.rb
+++ b/actionmailbox/app/jobs/action_mailbox/incineration_job.rb
@@ -6,6 +6,9 @@ module ActionMailbox
#
# Since this incineration is set for the future, it'll automatically ignore any <tt>InboundEmail</tt>s
# that have already been deleted and discard itself if so.
+ #
+ # You can disable incinerating processed emails by setting +config.action_mailbox.incinerate+ or
+ # +ActionMailbox.incinerate+ to +false+.
class IncinerationJob < ActiveJob::Base
queue_as { ActionMailbox.queues[:incineration] }
diff --git a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb
index 697331ede4..e7c8782f33 100644
--- a/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb
+++ b/actionmailbox/app/models/action_mailbox/inbound_email/incineratable.rb
@@ -7,7 +7,7 @@ module ActionMailbox::InboundEmail::Incineratable
extend ActiveSupport::Concern
included do
- after_update_commit :incinerate_later, if: -> { status_previously_changed? && processed? }
+ after_update_commit :incinerate_later, if: -> { ActionMailbox.incinerate && status_previously_changed? && processed? }
end
def incinerate_later
diff --git a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
index 89ab66c1a9..2bf4335808 100644
--- a/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
+++ b/actionmailbox/db/migrate/20180917164000_create_action_mailbox_tables.rb
@@ -5,11 +5,7 @@ class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
t.string :message_id, null: false
t.string :message_checksum, null: false
- if supports_datetime_with_precision?
- t.timestamps precision: 6
- else
- t.timestamps
- end
+ t.timestamps
t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
end
diff --git a/actionmailbox/lib/action_mailbox.rb b/actionmailbox/lib/action_mailbox.rb
index b4ff25a9ab..772dbd6529 100644
--- a/actionmailbox/lib/action_mailbox.rb
+++ b/actionmailbox/lib/action_mailbox.rb
@@ -11,6 +11,7 @@ module ActionMailbox
mattr_accessor :ingress
mattr_accessor :logger
+ mattr_accessor :incinerate, default: true
mattr_accessor :incinerate_after, default: 30.days
mattr_accessor :queues, default: {}
end
diff --git a/actionmailbox/lib/action_mailbox/engine.rb b/actionmailbox/lib/action_mailbox/engine.rb
index d5a07a7dce..27334c037e 100644
--- a/actionmailbox/lib/action_mailbox/engine.rb
+++ b/actionmailbox/lib/action_mailbox/engine.rb
@@ -14,6 +14,7 @@ module ActionMailbox
config.eager_load_namespaces << ActionMailbox
config.action_mailbox = ActiveSupport::OrderedOptions.new
+ config.action_mailbox.incinerate = true
config.action_mailbox.incinerate_after = 30.days
config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \
@@ -22,6 +23,7 @@ module ActionMailbox
initializer "action_mailbox.config" do
config.after_initialize do |app|
ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger
+ ActionMailbox.incinerate = app.config.action_mailbox.incinerate.nil? ? true : app.config.action_mailbox.incinerate
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
ActionMailbox.queues = app.config.action_mailbox.queues || {}
end
diff --git a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb
index 89ab66c1a9..2bf4335808 100644
--- a/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb
+++ b/actionmailbox/test/dummy/db/migrate/20180208205311_create_action_mailbox_tables.rb
@@ -5,11 +5,7 @@ class CreateActionMailboxTables < ActiveRecord::Migration[6.0]
t.string :message_id, null: false
t.string :message_checksum, null: false
- if supports_datetime_with_precision?
- t.timestamps precision: 6
- else
- t.timestamps
- end
+ t.timestamps
t.index [ :message_id, :message_checksum ], name: "index_action_mailbox_inbound_emails_uniqueness", unique: true
end
diff --git a/actionmailbox/test/dummy/db/schema.rb b/actionmailbox/test/dummy/db/schema.rb
index 10d4111a89..76c979bcec 100644
--- a/actionmailbox/test/dummy/db/schema.rb
+++ b/actionmailbox/test/dummy/db/schema.rb
@@ -42,4 +42,5 @@ ActiveRecord::Schema.define(version: 2018_02_12_164506) do
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
+ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
end
diff --git a/actionmailbox/test/unit/inbound_email/incineration_test.rb b/actionmailbox/test/unit/inbound_email/incineration_test.rb
index 21c01a9cea..54488349fd 100644
--- a/actionmailbox/test/unit/inbound_email/incineration_test.rb
+++ b/actionmailbox/test/unit/inbound_email/incineration_test.rb
@@ -44,4 +44,14 @@ class ActionMailbox::InboundEmail::IncinerationTest < ActiveSupport::TestCase
perform_enqueued_jobs only: ActionMailbox::IncinerationJob
end
end
+
+ test "skipping incineration" do
+ original, ActionMailbox.incinerate = ActionMailbox.incinerate, false
+
+ assert_no_enqueued_jobs only: ActionMailbox::IncinerationJob do
+ create_inbound_email_from_fixture("welcome.eml").delivered!
+ end
+ ensure
+ ActionMailbox.incinerate = original
+ end
end
diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index 23488db790..893a4a25b1 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -59,6 +59,14 @@ module ActionMailer
end
end
+ initializer "action_mailer.set_autoload_paths" do |app|
+ options = app.config.action_mailer
+
+ if options.show_previews && options.preview_path
+ ActiveSupport::Dependencies.autoload_paths << options.preview_path
+ end
+ end
+
initializer "action_mailer.compile_config_methods" do
ActiveSupport.on_load(:action_mailer) do
config.compile_methods! if config.respond_to?(:compile_methods!)
@@ -76,12 +84,8 @@ module ActionMailer
if options.show_previews
app.routes.prepend do
- get "/rails/mailers" => "rails/mailers#index", internal: true
- get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
- end
-
- if options.preview_path
- ActiveSupport::Dependencies.autoload_paths << options.preview_path
+ get "/rails/mailers" => "rails/mailers#index", internal: true
+ get "/rails/mailers/*path" => "rails/mailers#preview", internal: true
end
end
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 1d2f6b09c3..23ac619f50 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,13 @@
+* Make debug exceptions works in an environment where ActiveStorage is not loaded.
+
+ *Tomoyuki Kurosawa*
+
+* `ActionDispatch::SystemTestCase.driven_by` can now be called with a block
+ to define specific browser capabilities.
+
+ *Edouard Chin*
+
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
* Remove deprecated `fragment_cache_key` helper in favor of `combined_fragment_cache_key`.
@@ -14,7 +24,7 @@
* Introduce ActionDispatch::HostAuthorization
This is a new middleware that guards against DNS rebinding attacks by
- white-listing the allowed hosts a request can be made to.
+ explicitly permitting the hosts a request can be made to.
Each host is checked with the case operator (`#===`) to support `RegExp`,
`Proc`, `IPAddr` and custom objects as host allowances.
@@ -154,7 +164,7 @@
*Aaron Kromer*
-* Pass along arguments to underlying `get` method in `follow_redirect!`.
+* Pass along arguments to underlying `get` method in `follow_redirect!`
Now all arguments passed to `follow_redirect!` are passed to the underlying
`get` method. This for example allows to set custom headers for the
diff --git a/actionpack/lib/abstract_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb
index 4e454adc5f..18677ddd18 100644
--- a/actionpack/lib/abstract_controller/caching/fragments.rb
+++ b/actionpack/lib/abstract_controller/caching/fragments.rb
@@ -28,7 +28,6 @@ module AbstractController
self.fragment_cache_keys = []
if respond_to?(:helper_method)
- helper_method :fragment_cache_key
helper_method :combined_fragment_cache_key
end
end
diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb
index 30034be018..e1e0c6f456 100644
--- a/actionpack/lib/action_controller/metal/exceptions.rb
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -52,7 +52,7 @@ module ActionController
end
# Raised when a nested respond_to is triggered and the content types of each
- # are incompatible. For exampe:
+ # are incompatible. For example:
#
# respond_to do |outer_type|
# outer_type.js do
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
index 0faaac1ce4..f1fb7ab0f7 100644
--- a/actionpack/lib/action_controller/metal/helpers.rb
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -75,7 +75,7 @@ module ActionController
# Provides a proxy to access helper methods from outside the view.
def helpers
@helper_proxy ||= begin
- proxy = ActionView::Base.new
+ proxy = ActionView::Base.empty
proxy.config = config.inheritable_copy
proxy.extend(_helpers)
end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 04922b0715..815f82a1f2 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -795,7 +795,7 @@ module ActionController
@permitted = coder.map["ivars"][:@permitted]
when "!ruby/object:ActionController::Parameters"
# YAML's Object format. Only needed because of the format
- # backwardscompability above, otherwise equivalent to YAML's initialization.
+ # backwards compatibility above, otherwise equivalent to YAML's initialization.
@parameters, @permitted = coder.map["parameters"], coder.map["permitted"]
end
end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
index 537f479ee5..697f5b9d8b 100644
--- a/actionpack/lib/action_dispatch/journey/path/pattern.rb
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -119,7 +119,7 @@ module ActionDispatch
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
def accept(node)
- %r{\A#{visit node}}
+ %r{\A#{visit node}(?:\b|\Z)}
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index cb28baa229..1611a8b3dd 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -488,13 +488,8 @@ module ActionDispatch
end
def cookie_metadata(name, options)
- if request.use_cookies_with_metadata
- metadata = expiry_options(options)
- metadata[:purpose] = "cookie.#{name}"
-
- metadata
- else
- {}
+ expiry_options(options).tap do |metadata|
+ metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_view.rb b/actionpack/lib/action_dispatch/middleware/debug_view.rb
index ac12dc13a1..f16484d1ea 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_view.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_view.rb
@@ -10,7 +10,13 @@ module ActionDispatch
RESCUES_TEMPLATE_PATH = File.expand_path("templates", __dir__)
def initialize(assigns)
- super([RESCUES_TEMPLATE_PATH], assigns)
+ paths = [RESCUES_TEMPLATE_PATH]
+ renderer = ActionView::Renderer.new ActionView::LookupContext.new(paths)
+ super(renderer, assigns)
+ end
+
+ def compiled_method_container
+ self.class
end
def debug_params(params)
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index fb2b2bd3b0..1fb3e9db00 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -31,22 +31,34 @@ module ActionDispatch
"ActionController::MissingExactTemplate" => "missing_exact_template",
)
+ cattr_accessor :wrapper_exceptions, default: [
+ "ActionView::Template::Error"
+ ]
+
attr_reader :backtrace_cleaner, :exception, :wrapped_causes, :line_number, :file
def initialize(backtrace_cleaner, exception)
@backtrace_cleaner = backtrace_cleaner
- @exception = original_exception(exception)
+ @exception = exception
@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)
+ exception.cause
+ else
+ exception
+ end
+ end
+
def rescue_template
@@rescue_templates[@exception.class.name]
end
def status_code
- self.class.status_code_for_exception(@exception.class.name)
+ self.class.status_code_for_exception(unwrapped_exception.class.name)
end
def application_trace
@@ -122,14 +134,6 @@ module ActionDispatch
Array(@exception.backtrace)
end
- def original_exception(exception)
- if @@rescue_responses.has_key?(exception.cause.class.name)
- exception.cause
- else
- exception
- end
- end
-
def causes_for(exception)
return enum_for(__method__, exception) unless block_given?
diff --git a/actionpack/lib/action_dispatch/middleware/host_authorization.rb b/actionpack/lib/action_dispatch/middleware/host_authorization.rb
index 447b70112a..b7dff1df41 100644
--- a/actionpack/lib/action_dispatch/middleware/host_authorization.rb
+++ b/actionpack/lib/action_dispatch/middleware/host_authorization.rb
@@ -3,8 +3,8 @@
require "action_dispatch/http/request"
module ActionDispatch
- # This middleware guards from DNS rebinding attacks by white-listing the
- # hosts a request can be sent to.
+ # This middleware guards from DNS rebinding attacks by explicitly permitting
+ # the hosts a request can be sent to.
#
# When a request comes to an unauthorized host, the +response_app+
# application will be executed and rendered. If no +response_app+ is given, a
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index 3c88afd4d3..767143a368 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -45,7 +45,7 @@ module ActionDispatch
backtrace_cleaner = request.get_header "action_dispatch.backtrace_cleaner"
wrapper = ExceptionWrapper.new(backtrace_cleaner, exception)
status = wrapper.status_code
- request.set_header "action_dispatch.exception", wrapper.exception
+ request.set_header "action_dispatch.exception", wrapper.unwrapped_exception
request.set_header "action_dispatch.original_path", request.path_info
request.path_info = "/#{status}"
response = @exceptions_app.call(request.env)
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb
index 2fa78dd385..1fbc107e28 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb
@@ -2,6 +2,6 @@
<h1>Blocked host: <%= @host %></h1>
</header>
<div id="container">
- <h2>To allow requests to <%= @host %>, add the following configuration:</h2>
- <pre>Rails.application.config.hosts &lt;&lt; "<%= @host %>"</pre>
+ <h2>To allow requests to <%= @host %>, add the following to your environment configuration:</h2>
+ <pre>config.hosts &lt;&lt; "<%= @host %>"</pre>
</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb
index 4e2d1d0b08..a94dd982a7 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb
@@ -1,5 +1,5 @@
Blocked host: <%= @host %>
-To allow requests to <%= @host %>, add the following configuration:
+To allow requests to <%= @host %>, add the following to your environment configuration:
- Rails.application.config.hosts << "<%= @host %>"
+ config.hosts << "<%= @host %>"
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
index e8454acfad..d144fc1cd2 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb
@@ -10,7 +10,7 @@
<div id="container">
<h2>
<%= h @exception.message %>
- <% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %>
+ <% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
<br />To resolve this issue run: rails active_storage:install
<% end %>
</h2>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
index e5e3196710..55aaf58713 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb
@@ -4,7 +4,7 @@
<% end %>
<%= @exception.message %>
-<% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %>
+<% if defined?(ActiveStorage) && @exception.message.match?(%r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}}) %>
To resolve this issue run: rails active_storage:install
<% end %>
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 5cde677051..f832719f19 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -74,8 +74,8 @@ module ActionDispatch
# For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper
# methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>.
#
- # get 'post/:id' => 'posts#show'
- # post 'post/:id' => 'posts#create_comment'
+ # get 'post/:id', to: 'posts#show'
+ # post 'post/:id', to: 'posts#create_comment'
#
# Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
# URL will route to the <tt>show</tt> action.
@@ -83,7 +83,7 @@ module ActionDispatch
# If your route needs to respond to more than one HTTP method (or all methods) then using the
# <tt>:via</tt> option on <tt>match</tt> is preferable.
#
- # match 'post/:id' => 'posts#show', via: [:get, :post]
+ # match 'post/:id', to: 'posts#show', via: [:get, :post]
#
# == Named routes
#
@@ -94,7 +94,7 @@ module ActionDispatch
# Example:
#
# # In config/routes.rb
- # get '/login' => 'accounts#login', as: 'login'
+ # get '/login', to: 'accounts#login', as: 'login'
#
# # With render, redirect_to, tests, etc.
# redirect_to login_url
@@ -120,9 +120,9 @@ module ActionDispatch
#
# # In config/routes.rb
# controller :blog do
- # get 'blog/show' => :list
- # get 'blog/delete' => :delete
- # get 'blog/edit' => :edit
+ # get 'blog/show', to: :list
+ # get 'blog/delete', to: :delete
+ # get 'blog/edit', to: :edit
# end
#
# # provides named routes for show, delete, and edit
@@ -132,7 +132,7 @@ module ActionDispatch
#
# Routes can generate pretty URLs. For example:
#
- # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: {
+ # get '/articles/:year/:month/:day', to: 'articles#find_by_id', constraints: {
# year: /\d{4}/,
# month: /\d{1,2}/,
# day: /\d{1,2}/
@@ -147,7 +147,7 @@ module ActionDispatch
# You can specify a regular expression to define a format for a parameter.
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /\d{5}(-\d{4})?/
# }
# end
@@ -156,13 +156,13 @@ module ActionDispatch
# expression modifiers:
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /hx\d\d\s\d[a-z]{2}/i
# }
# end
#
# controller 'geocode' do
- # get 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode', to: :show, constraints: {
# postalcode: /# Postalcode format
# \d{5} #Prefix
# (-\d{4})? #Suffix
@@ -178,13 +178,13 @@ module ActionDispatch
#
# You can redirect any path to another path using the redirect helper in your router:
#
- # get "/stories" => redirect("/posts")
+ # get "/stories", to: redirect("/posts")
#
# == Unicode character routes
#
# You can specify unicode character routes in your router:
#
- # get "こんにちは" => "welcome#index"
+ # get "こんにちは", to: "welcome#index"
#
# == Routing to Rack Applications
#
@@ -192,7 +192,7 @@ module ActionDispatch
# index action in the PostsController, you can specify any Rack application
# as the endpoint for a matcher:
#
- # get "/application.js" => Sprockets
+ # get "/application.js", to: Sprockets
#
# == Reloading routes
#
diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb
index c74c0ccced..066daa4a12 100644
--- a/actionpack/lib/action_dispatch/system_test_case.rb
+++ b/actionpack/lib/action_dispatch/system_test_case.rb
@@ -89,6 +89,24 @@ module ActionDispatch
# { js_errors: true }
# end
#
+ # Some drivers require browser capabilities to be passed as a block instead
+ # of through the +options+ hash.
+ #
+ # As an example, if you want to add mobile emulation on chrome, you'll have to
+ # create an instance of selenium's +Chrome::Options+ object and add
+ # capabilities with a block.
+ #
+ # The block will be passed an instance of <tt><Driver>::Options</tt> where you can
+ # define the capabilities you want. Please refer to your driver documentation
+ # to learn about supported options.
+ #
+ # class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
+ # driven_by :selenium, using: :chrome, screen_size: [1024, 768] do |driver_option|
+ # driver_option.add_emulation(device_name: 'iPhone 6')
+ # driver_option.add_extension('path/to/chrome_extension.crx')
+ # end
+ # end
+ #
# Because <tt>ActionDispatch::SystemTestCase</tt> is a shim between Capybara
# and Rails, any driver that is supported by Capybara is supported by system
# tests as long as you include the required gems and files.
@@ -134,8 +152,10 @@ module ActionDispatch
# driven_by :selenium, using: :firefox
#
# driven_by :selenium, using: :headless_firefox
- def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {})
- self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options)
+ def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}, &capabilities)
+ driver_options = { using: using, screen_size: screen_size, options: options }
+
+ self.driver = SystemTesting::Driver.new(driver, driver_options, &capabilities)
end
driven_by :selenium
diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb
index 1b0bce6b9e..c34907b6cb 100644
--- a/actionpack/lib/action_dispatch/system_testing/browser.rb
+++ b/actionpack/lib/action_dispatch/system_testing/browser.rb
@@ -29,20 +29,28 @@ module ActionDispatch
end
end
+ def capabilities
+ @option ||=
+ case type
+ when :chrome
+ ::Selenium::WebDriver::Chrome::Options.new
+ when :firefox
+ ::Selenium::WebDriver::Firefox::Options.new
+ end
+ end
+
private
def headless_chrome_browser_options
- options = Selenium::WebDriver::Chrome::Options.new
- options.args << "--headless"
- options.args << "--disable-gpu" if Gem.win_platform?
+ capabilities.args << "--headless"
+ capabilities.args << "--disable-gpu" if Gem.win_platform?
- options
+ capabilities
end
def headless_firefox_browser_options
- options = Selenium::WebDriver::Firefox::Options.new
- options.args << "-headless"
+ capabilities.args << "-headless"
- options
+ capabilities
end
end
end
diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb
index 5252ff6746..25a09dd918 100644
--- a/actionpack/lib/action_dispatch/system_testing/driver.rb
+++ b/actionpack/lib/action_dispatch/system_testing/driver.rb
@@ -3,11 +3,12 @@
module ActionDispatch
module SystemTesting
class Driver # :nodoc:
- def initialize(name, **options)
+ def initialize(name, **options, &capabilities)
@name = name
@browser = Browser.new(options[:using])
@screen_size = options[:screen_size]
@options = options[:options]
+ @capabilities = capabilities
end
def use
@@ -22,6 +23,8 @@ module ActionDispatch
end
def register
+ define_browser_capabilities(@browser.capabilities)
+
Capybara.register_driver @name do |app|
case @name
when :selenium then register_selenium(app)
@@ -31,6 +34,10 @@ module ActionDispatch
end
end
+ def define_browser_capabilities(capabilities)
+ @capabilities.call(capabilities) if @capabilities
+ end
+
def browser_options
@options.merge(options: @browser.options).compact
end
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 884fb51d18..79359a0c8b 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
@@ -20,7 +20,7 @@ module ActionDispatch
# * [+inline+] Display the screenshot in the terminal using the
# iTerm image protocol (https://iterm2.com/documentation-images.html).
# * [+artifact+] Display the screenshot in the terminal, using the terminal
- # artifact format (https://buildkite.github.io/terminal/inline-images/).
+ # artifact format (https://buildkite.github.io/terminal-to-html/inline-images/).
def take_screenshot
save_image
puts display_image
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 45439a3bb1..10d85037ae 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -335,7 +335,7 @@ module ActionDispatch
klass = APP_SESSIONS[app] ||= Class.new(Integration::Session) {
# If the app is a Rails app, make url_helpers available on the session.
# This makes app.url_for and app.foo_path available in the console.
- if app.respond_to?(:routes)
+ if app.respond_to?(:routes) && app.routes.is_a?(ActionDispatch::Routing::RouteSet)
include app.routes.url_helpers
include app.routes.mounted_helpers
end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 998498e1b2..7f1c41787a 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -68,10 +68,18 @@ class RedirectController < ActionController::Base
redirect_back(fallback_location: "/things/stuff", status: 307)
end
+ def redirect_back_with_status_and_fallback_location_to_another_host
+ redirect_back(fallback_location: "http://www.rubyonrails.org/", status: 307)
+ end
+
def safe_redirect_back_with_status
redirect_back(fallback_location: "/things/stuff", status: 307, allow_other_host: false)
end
+ def safe_redirect_back_with_status_and_fallback_location_to_another_host
+ redirect_back(fallback_location: "http://www.rubyonrails.org/", status: 307, allow_other_host: false)
+ end
+
def host_redirect
redirect_to action: "other_host", only_path: false, host: "other.test.host"
end
@@ -280,6 +288,13 @@ class RedirectTest < ActionController::TestCase
assert_equal "http://test.host/things/stuff", redirect_to_url
end
+ def test_redirect_back_with_no_referer_redirects_to_another_host
+ get :redirect_back_with_status_and_fallback_location_to_another_host
+
+ assert_response 307
+ assert_equal "http://www.rubyonrails.org/", redirect_to_url
+ end
+
def test_safe_redirect_back_from_other_host
@request.env["HTTP_REFERER"] = "http://another.host/coming/from"
get :safe_redirect_back_with_status
@@ -297,6 +312,20 @@ class RedirectTest < ActionController::TestCase
assert_equal referer, redirect_to_url
end
+ def test_safe_redirect_back_with_no_referer
+ get :safe_redirect_back_with_status
+
+ assert_response 307
+ assert_equal "http://test.host/things/stuff", redirect_to_url
+ end
+
+ def test_safe_redirect_back_with_no_referer_redirects_to_another_host
+ get :safe_redirect_back_with_status_and_fallback_location_to_another_host
+
+ assert_response 307
+ assert_equal "http://www.rubyonrails.org/", redirect_to_url
+ end
+
def test_redirect_to_record
with_routing do |set|
set.draw do
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 8f9dbaf4b3..4aaac1320e 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -1193,11 +1193,7 @@ class CookiesTest < ActionController::TestCase
get :encrypted_discount_and_user_id_cookie
travel 2.hours
- assert_equal 50, cookies.encrypted[:user_id]
-
- cookies[:discount_percentage] = cookies[:user_id]
- assert_not_equal 10, cookies.encrypted[:discount_percentage]
- assert_equal 50, cookies.encrypted[:discount_percentage]
+ assert_nil cookies.signed[:user_id]
end
def test_switch_off_metadata_for_signed_cookies_if_config_is_false
@@ -1206,11 +1202,8 @@ class CookiesTest < ActionController::TestCase
get :signed_discount_and_user_id_cookie
travel 2.hours
- assert_equal 50, cookies.signed[:user_id]
- cookies[:discount_percentage] = cookies[:user_id]
- assert_not_equal 10, cookies.signed[:discount_percentage]
- assert_equal 50, cookies.signed[:discount_percentage]
+ assert_nil cookies.signed[:user_id]
end
def test_read_rails_5_2_stable_encrypted_cookies_if_config_is_false
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index aadc6be077..6914fb66f9 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -39,52 +39,56 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
def call(env)
env["action_dispatch.show_detailed_exceptions"] = @detailed
req = ActionDispatch::Request.new(env)
+ template = ActionView::Template.new(File.read(__FILE__), __FILE__, ActionView::Template::Handlers::Raw.new, {})
+
case req.path
- when %r{/pass}
+ when "/pass"
[404, { "X-Cascade" => "pass" }, self]
- when %r{/not_found}
+ when "/not_found"
raise AbstractController::ActionNotFound
- when %r{/runtime_error}
+ when "/runtime_error"
raise RuntimeError
- when %r{/method_not_allowed}
+ when "/method_not_allowed"
raise ActionController::MethodNotAllowed
- when %r{/intercepted_error}
+ when "/intercepted_error"
raise InterceptedErrorInstance
- when %r{/unknown_http_method}
+ when "/unknown_http_method"
raise ActionController::UnknownHttpMethod
- when %r{/not_implemented}
+ when "/not_implemented"
raise ActionController::NotImplemented
- when %r{/unprocessable_entity}
+ when "/unprocessable_entity"
raise ActionController::InvalidAuthenticityToken
- when %r{/not_found_original_exception}
+ when "/not_found_original_exception"
begin
raise AbstractController::ActionNotFound.new
rescue
- raise ActionView::Template::Error.new("template")
+ raise ActionView::Template::Error.new(template)
+ end
+ when "/cause_mapped_to_rescue_responses"
+ begin
+ raise ActionController::ParameterMissing, :missing_param_key
+ rescue
+ raise NameError.new("uninitialized constant Userr")
end
- when %r{/missing_template}
+ when "/missing_template"
raise ActionView::MissingTemplate.new(%w(foo), "foo/index", %w(foo), false, "mailer")
- when %r{/bad_request}
+ when "/bad_request"
raise ActionController::BadRequest
- when %r{/missing_keys}
+ when "/missing_keys"
raise ActionController::UrlGenerationError, "No route matches"
- when %r{/parameter_missing}
+ when "/parameter_missing"
raise ActionController::ParameterMissing, :missing_param_key
- when %r{/original_syntax_error}
+ when "/original_syntax_error"
eval "broke_syntax =" # `eval` need for raise native SyntaxError at runtime
- when %r{/syntax_error_into_view}
+ when "/syntax_error_into_view"
begin
eval "broke_syntax ="
rescue Exception
- template = ActionView::Template.new(File.read(__FILE__),
- __FILE__,
- ActionView::Template::Handlers::Raw.new,
- {})
raise ActionView::Template::Error.new(template)
end
- when %r{/framework_raises}
+ when "/framework_raises"
method_that_raises
- when %r{/nested_exceptions}
+ when "/nested_exceptions"
raise_nested_exceptions
else
raise "puke!"
@@ -313,12 +317,22 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_match("&quot;foo&quot;=&gt;&quot;[FILTERED]&quot;", body)
end
- test "show registered original exception for wrapped exceptions" do
+ test "show registered original exception if the last exception is TemplateError" do
@app = DevelopmentApp
get "/not_found_original_exception", headers: { "action_dispatch.show_exceptions" => true }
assert_response 404
- assert_match(/AbstractController::ActionNotFound/, body)
+ assert_match %r{AbstractController::ActionNotFound}, body
+ assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body
+ end
+
+ test "show the last exception and cause even when the cause is mapped to resque_responses" do
+ @app = DevelopmentApp
+
+ get "/cause_mapped_to_rescue_responses", headers: { "action_dispatch.show_exceptions" => true }
+ assert_response 500
+ assert_match %r{ActionController::ParameterMissing}, body
+ assert_match %r{NameError}, body
end
test "named urls missing keys raise 500 level error" do
@@ -480,6 +494,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_select "#Application-Trace-0" do
assert_select "code", /syntax error, unexpected/
end
+ assert_match %r{Showing <i>.*test/dispatch/debug_exceptions_test.rb</i>}, body
end
test "debug exceptions app shows user code that caused the error in source view" do
diff --git a/actionpack/test/dispatch/host_authorization_test.rb b/actionpack/test/dispatch/host_authorization_test.rb
index dae7b08ec1..5263dd2597 100644
--- a/actionpack/test/dispatch/host_authorization_test.rb
+++ b/actionpack/test/dispatch/host_authorization_test.rb
@@ -15,7 +15,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
assert_match "Blocked host: www.example.com", response.body
end
- test "passes all requests to if the whitelist is empty" do
+ test "allows all requests if hosts is empty" do
@app = ActionDispatch::HostAuthorization.new(App, nil)
get "/"
@@ -24,7 +24,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
assert_equal "Success", body
end
- test "passes requests to allowed host" do
+ test "hosts can be a single element array" do
@app = ActionDispatch::HostAuthorization.new(App, %w(www.example.com))
get "/"
@@ -33,7 +33,7 @@ class HostAuthorizationTest < ActionDispatch::IntegrationTest
assert_equal "Success", body
end
- test "the whitelist could be a single element" do
+ test "hosts can be a string" do
@app = ActionDispatch::HostAuthorization.new(App, "www.example.com")
get "/"
diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb
index f6cf653980..e42ea89f6f 100644
--- a/actionpack/test/dispatch/mount_test.rb
+++ b/actionpack/test/dispatch/mount_test.rb
@@ -80,6 +80,12 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
assert_equal "/shorthand -- /omg", response.body
end
+ def test_mounting_does_not_match_similar_paths
+ get "/shorthandomg"
+ assert_not_equal "/shorthand -- /omg", response.body
+ assert_equal " -- /shorthandomg", response.body
+ end
+
def test_mounting_works_with_via
get "/getfake"
assert_equal "OK", response.body
diff --git a/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb b/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb
new file mode 100644
index 0000000000..676a8c38d4
--- /dev/null
+++ b/actionpack/test/dispatch/routing/non_dispatch_routed_app_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require "abstract_unit"
+
+module ActionDispatch
+ module Routing
+ class NonDispatchRoutedAppTest < ActionDispatch::IntegrationTest
+ # For example, Grape::API
+ class SimpleApp
+ def self.call(env)
+ [ 200, { "Content-Type" => "text/plain" }, [] ]
+ end
+
+ def self.routes
+ []
+ end
+ end
+
+ setup { @app = SimpleApp }
+
+ test "does not except" do
+ get "/foo"
+ assert_response :success
+ end
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb
index 9b51ee1cad..ac685a7dca 100644
--- a/actionpack/test/dispatch/session/mem_cache_store_test.rb
+++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb
@@ -38,8 +38,9 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
begin
require "dalli"
- ss = Dalli::Client.new("localhost:11211").stats
- raise Dalli::DalliError unless ss["localhost:11211"]
+ servers = ENV["MEMCACHE_SERVERS"] || "localhost:11211"
+ ss = Dalli::Client.new(servers).stats
+ raise Dalli::DalliError unless ss[servers]
def test_setting_and_getting_session_value
with_test_route_set do
@@ -195,7 +196,9 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
end
@app = self.class.build_app(set) do |middleware|
- middleware.use ActionDispatch::Session::MemCacheStore, key: "_session_id", namespace: "mem_cache_store_test:#{SecureRandom.hex(10)}"
+ middleware.use ActionDispatch::Session::MemCacheStore,
+ key: "_session_id", namespace: "mem_cache_store_test:#{SecureRandom.hex(10)}",
+ memcache_server: ENV["MEMCACHE_SERVERS"] || "localhost:11211"
middleware.delete ActionDispatch::ShowExceptions
end
diff --git a/actionpack/test/dispatch/system_testing/driver_test.rb b/actionpack/test/dispatch/system_testing/driver_test.rb
index a824ee0c84..0d08f17af3 100644
--- a/actionpack/test/dispatch/system_testing/driver_test.rb
+++ b/actionpack/test/dispatch/system_testing/driver_test.rb
@@ -2,6 +2,7 @@
require "abstract_unit"
require "action_dispatch/system_testing/driver"
+require "selenium/webdriver"
class DriverTest < ActiveSupport::TestCase
test "initializing the driver" do
@@ -22,6 +23,7 @@ class DriverTest < ActiveSupport::TestCase
driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_chrome, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" })
assert_equal :selenium, driver.instance_variable_get(:@name)
assert_equal :headless_chrome, driver.instance_variable_get(:@browser).name
+ assert_instance_of Selenium::WebDriver::Chrome::Options, driver.instance_variable_get(:@browser).options
assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
end
@@ -30,6 +32,7 @@ class DriverTest < ActiveSupport::TestCase
driver = ActionDispatch::SystemTesting::Driver.new(:selenium, using: :headless_firefox, screen_size: [1400, 1400], options: { url: "http://example.com/wd/hub" })
assert_equal :selenium, driver.instance_variable_get(:@name)
assert_equal :headless_firefox, driver.instance_variable_get(:@browser).name
+ assert_instance_of Selenium::WebDriver::Firefox::Options, driver.instance_variable_get(:@browser).options
assert_equal [1400, 1400], driver.instance_variable_get(:@screen_size)
assert_equal ({ url: "http://example.com/wd/hub" }), driver.instance_variable_get(:@options)
end
@@ -51,4 +54,70 @@ class DriverTest < ActiveSupport::TestCase
test "registerable? returns false if driver is rack_test" do
assert_not ActionDispatch::SystemTesting::Driver.new(:rack_test).send(:registerable?)
end
+
+ test "define extra capabilities using chrome" do
+ driver_option = nil
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :chrome) do |option|
+ option.add_argument("start-maximized")
+ option.add_emulation(device_name: "iphone 6")
+ option.add_preference(:detach, true)
+
+ driver_option = option
+ end
+ driver.use
+
+ expected = { args: ["start-maximized"], mobileEmulation: { deviceName: "iphone 6" }, prefs: { detach: true } }
+ assert_equal expected, driver_option.as_json
+ end
+
+ test "define extra capabilities using headless_chrome" do
+ driver_option = nil
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :headless_chrome) do |option|
+ option.add_argument("start-maximized")
+ option.add_emulation(device_name: "iphone 6")
+ option.add_preference(:detach, true)
+
+ driver_option = option
+ end
+ driver.use
+
+ expected = { args: ["start-maximized"], mobileEmulation: { deviceName: "iphone 6" }, prefs: { detach: true } }
+ assert_equal expected, driver_option.as_json
+ end
+
+ test "define extra capabilities using firefox" do
+ driver_option = nil
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :firefox) do |option|
+ option.add_preference("browser.startup.homepage", "http://www.seleniumhq.com/")
+ option.add_argument("--host=127.0.0.1")
+
+ driver_option = option
+ end
+ driver.use
+
+ expected = { "moz:firefoxOptions" => { args: ["--host=127.0.0.1"], prefs: { "browser.startup.homepage" => "http://www.seleniumhq.com/" } } }
+ assert_equal expected, driver_option.as_json
+ end
+
+ test "define extra capabilities using headless_firefox" do
+ driver_option = nil
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :headless_firefox) do |option|
+ option.add_preference("browser.startup.homepage", "http://www.seleniumhq.com/")
+ option.add_argument("--host=127.0.0.1")
+
+ driver_option = option
+ end
+ driver.use
+
+ expected = { "moz:firefoxOptions" => { args: ["--host=127.0.0.1"], prefs: { "browser.startup.homepage" => "http://www.seleniumhq.com/" } } }
+ assert_equal expected, driver_option.as_json
+ end
+
+ test "does not define extra capabilities" do
+ driver = ActionDispatch::SystemTesting::Driver.new(:selenium, screen_size: [1400, 1400], using: :firefox)
+
+ assert_nothing_raised do
+ driver.use
+ end
+ 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 097ef8af29..b756b91379 100644
--- a/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
+++ b/actionpack/test/dispatch/system_testing/screenshot_helper_test.rb
@@ -3,6 +3,7 @@
require "abstract_unit"
require "action_dispatch/system_testing/test_helpers/screenshot_helper"
require "capybara/dsl"
+require "selenium/webdriver"
class ScreenshotHelperTest < ActiveSupport::TestCase
test "image path is saved in tmp directory" do
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 b078a5abc5..847b09dcfe 100644
--- a/actionpack/test/dispatch/system_testing/system_test_case_test.rb
+++ b/actionpack/test/dispatch/system_testing/system_test_case_test.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "abstract_unit"
+require "selenium/webdriver"
class SetDriverToRackTestTest < DrivenByRackTest
test "uses rack_test" do
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb
index 3e7aea57f1..fcfaba96b0 100644
--- a/actionpack/test/journey/path/pattern_test.rb
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -34,17 +34,17 @@ module ActionDispatch
end
{
- "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?},
- "/:controller/foo" => %r{\A/(#{x})/foo},
- "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)},
- "/:controller" => %r{\A/(#{x})},
- "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?},
- "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml},
- "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)},
- "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?},
- "/:controller/*foo" => %r{\A/(#{x})/(.+)},
- "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar},
- "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))},
+ "/:controller(/:action)" => %r{\A/(#{x})(?:/([^/.?]+))?(?:\b|\Z)},
+ "/:controller/foo" => %r{\A/(#{x})/foo(?:\b|\Z)},
+ "/:controller/:action" => %r{\A/(#{x})/([^/.?]+)(?:\b|\Z)},
+ "/:controller" => %r{\A/(#{x})(?:\b|\Z)},
+ "/:controller(/:action(/:id))" => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?(?:\b|\Z)},
+ "/:controller/:action.xml" => %r{\A/(#{x})/([^/.?]+)\.xml(?:\b|\Z)},
+ "/:controller.:format" => %r{\A/(#{x})\.([^/.?]+)(?:\b|\Z)},
+ "/:controller(.:format)" => %r{\A/(#{x})(?:\.([^/.?]+))?(?:\b|\Z)},
+ "/:controller/*foo" => %r{\A/(#{x})/(.+)(?:\b|\Z)},
+ "/:controller/*foo/bar" => %r{\A/(#{x})/(.+)/bar(?:\b|\Z)},
+ "/:foo|*bar" => %r{\A/(?:([^/.?]+)|(.+))(?:\b|\Z)},
}.each do |path, expected|
define_method(:"test_to_non_anchored_regexp_#{Regexp.escape(path)}") do
path = Pattern.build(
diff --git a/actiontext/app/helpers/action_text/tag_helper.rb b/actiontext/app/helpers/action_text/tag_helper.rb
index 837b2264b1..8434f2c611 100644
--- a/actiontext/app/helpers/action_text/tag_helper.rb
+++ b/actiontext/app/helpers/action_text/tag_helper.rb
@@ -4,7 +4,7 @@ module ActionText
module TagHelper
cattr_accessor(:id, instance_accessor: false) { 0 }
- # Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as well as a hidden field
+ # Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field
# that Trix will write to on changes, so the content will be sent on form submissions.
#
# ==== Options
@@ -50,7 +50,7 @@ module ActionView::Helpers
end
module FormHelper
- # Returns a `trix-editor` tag that instantiates the Trix JavaScript editor as well as a hidden field
+ # Returns a +trix-editor+ tag that instantiates the Trix JavaScript editor as well as a hidden field
# that Trix will write to on changes, so the content will be sent on form submissions.
#
# ==== Options
diff --git a/actiontext/app/models/action_text/rich_text.rb b/actiontext/app/models/action_text/rich_text.rb
index 705dd30983..19fa3e030e 100644
--- a/actiontext/app/models/action_text/rich_text.rb
+++ b/actiontext/app/models/action_text/rich_text.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module ActionText
- # The RichText record holds the content produced by the Trix editor in a serialized `body` attribute.
+ # The RichText record holds the content produced by the Trix editor in a serialized +body+ attribute.
# It also holds all the references to the embedded files, which are stored using Active Storage.
# This record is then associated with the Active Record model the application desires to have
- # rich text content using the `has_rich_text` class method.
+ # rich text content using the +has_rich_text+ class method.
class RichText < ActiveRecord::Base
self.table_name = "action_text_rich_texts"
@@ -15,7 +15,7 @@ module ActionText
has_many_attached :embeds
before_save do
- self.embeds = body.attachments.map(&:attachable) if body.present?
+ self.embeds = body.attachables.grep(ActiveStorage::Blob) if body.present?
end
def to_plain_text
diff --git a/actiontext/app/views/active_storage/blobs/_blob.html.erb b/actiontext/app/views/active_storage/blobs/_blob.html.erb
index 049f57e804..49ba357dd1 100644
--- a/actiontext/app/views/active_storage/blobs/_blob.html.erb
+++ b/actiontext/app/views/active_storage/blobs/_blob.html.erb
@@ -1,6 +1,6 @@
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
- <%= image_tag blob.representation(resize_to_fit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
+ <%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
<% end %>
<figcaption class="attachment__caption">
diff --git a/actiontext/db/migrate/201805281641_create_action_text_tables.rb b/actiontext/db/migrate/20180528164100_create_action_text_tables.rb
index 74c7a0ecb9..e7c66ea6ae 100644
--- a/actiontext/db/migrate/201805281641_create_action_text_tables.rb
+++ b/actiontext/db/migrate/20180528164100_create_action_text_tables.rb
@@ -2,11 +2,10 @@ class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
create_table :action_text_rich_texts do |t|
t.string :name, null: false
- t.text :body, limit: 16777215
+ t.text :body, size: :long
t.references :record, null: false, polymorphic: true, index: false
- t.datetime :created_at, null: false
- t.datetime :updated_at, null: false
+ t.timestamps
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end
diff --git a/actiontext/lib/templates/installer.rb b/actiontext/lib/templates/installer.rb
index 990e41ca00..a8000eb9fc 100644
--- a/actiontext/lib/templates/installer.rb
+++ b/actiontext/lib/templates/installer.rb
@@ -26,7 +26,7 @@ if APPLICATION_PACK_PATH.exist?
line = %[require("#{name}")]
unless APPLICATION_PACK_PATH.read.include? line
say "Adding #{name} to #{APPLICATION_PACK_PATH}"
- append_to_file APPLICATION_PACK_PATH, "#{line}\n"
+ append_to_file APPLICATION_PACK_PATH, "\n#{line}"
end
end
end
diff --git a/actiontext/test/dummy/db/migrate/2018052816_create_action_text_tables.rb b/actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb
index 74c7a0ecb9..e7c66ea6ae 100644
--- a/actiontext/test/dummy/db/migrate/2018052816_create_action_text_tables.rb
+++ b/actiontext/test/dummy/db/migrate/20180528164100_create_action_text_tables.rb
@@ -2,11 +2,10 @@ class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
create_table :action_text_rich_texts do |t|
t.string :name, null: false
- t.text :body, limit: 16777215
+ t.text :body, size: :long
t.references :record, null: false, polymorphic: true, index: false
- t.datetime :created_at, null: false
- t.datetime :updated_at, null: false
+ t.timestamps
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end
diff --git a/actiontext/test/dummy/db/schema.rb b/actiontext/test/dummy/db/schema.rb
index 7f8f4dff4e..2388986835 100644
--- a/actiontext/test/dummy/db/schema.rb
+++ b/actiontext/test/dummy/db/schema.rb
@@ -14,11 +14,11 @@ ActiveRecord::Schema.define(version: 2018_10_03_185713) do
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
- t.text "body", limit: 16777215
+ t.text "body"
t.string "record_type", null: false
t.integer "record_id", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end
@@ -45,14 +45,15 @@ ActiveRecord::Schema.define(version: 2018_10_03_185713) do
create_table "messages", force: :cascade do |t|
t.string "subject"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
end
create_table "people", force: :cascade do |t|
t.string "name"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", precision: 6, null: false
+ t.datetime "updated_at", precision: 6, null: false
end
+ add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
end
diff --git a/actiontext/test/unit/model_test.rb b/actiontext/test/unit/model_test.rb
index 122a20700b..d56363adc0 100644
--- a/actiontext/test/unit/model_test.rb
+++ b/actiontext/test/unit/model_test.rb
@@ -35,6 +35,15 @@ class ActionText::ModelTest < ActiveSupport::TestCase
assert_equal "racecar.jpg", message.content.embeds.first.filename.to_s
end
+ test "embed extraction only extracts file attachments" do
+ remote_image_html = '<action-text-attachment content-type="image" url="http://example.com/cat.jpg"></action-text-attachment>'
+ blob = create_file_blob(filename: "racecar.jpg", content_type: "image/jpg")
+ content = ActionText::Content.new(remote_image_html).append_attachables(blob)
+ message = Message.create!(subject: "Greetings", content: content)
+ assert_equal [ActionText::Attachables::RemoteImage, ActiveStorage::Blob], message.content.body.attachables.map(&:class)
+ assert_equal [ActiveStorage::Attachment], message.content.embeds.map(&:class)
+ end
+
test "saving content" do
message = Message.create!(subject: "Greetings", content: "<h1>Hello world</h1>")
assert_equal "Hello world", message.content.to_plain_text
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 16361fd2eb..5e7b271fb9 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,5 +1,27 @@
+* ActionView::Template.finalize_compiled_template_methods is deprecated with
+ no replacement.
+
+ *tenderlove*
+
+* config.action_view.finalize_compiled_template_methods is deprecated with
+ no replacement.
+
+ *tenderlove*
+
+* Ensure unique DOM IDs for collection inputs with float values.
+ Fixes #34974
+
+ *Mark Edmondson*
+
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
+* [Rename npm package](https://github.com/rails/rails/pull/34905) from
+ [`rails-ujs`](https://www.npmjs.com/package/rails-ujs) to
+ [`@rails/ujs`](https://www.npmjs.com/package/@rails/ujs).
+
+ *Javan Makhmali*
+
* Remove deprecated `image_alt` helper.
*Rafael Mendonça França*
diff --git a/actionview/Rakefile b/actionview/Rakefile
index 7851a2b6bf..237e458b6f 100644
--- a/actionview/Rakefile
+++ b/actionview/Rakefile
@@ -31,9 +31,26 @@ namespace :test do
desc "Run tests for rails-ujs"
task :ujs do
+ system("npm run lint")
+ exit $?.exitstatus unless $?.success?
+
begin
+ listen_host = "localhost"
+ listen_port = "4567"
+
+ runner_command = %w(ruby ../ci/qunit-selenium-runner.rb)
+ if ENV["SELENIUM_DRIVER_URL"]
+ require "socket"
+ runner_command += %W(http://#{Socket.gethostname}:#{listen_port}/ #{ENV["SELENIUM_DRIVER_URL"]})
+ listen_host = "0.0.0.0"
+ else
+ runner_command += %W(http://localhost:#{listen_port}/)
+ end
+
Dir.mkdir("log")
- pid = spawn("bundle exec rackup test/ujs/config.ru -p 4567 -s puma > log/test.log 2>&1", pgroup: true)
+ pid = File.open("log/test.log", "w") do |f|
+ spawn(*%W(rackup test/ujs/config.ru -o #{listen_host} -p #{listen_port} -s puma), out: f, err: f, pgroup: true)
+ end
start_time = Time.now
@@ -41,12 +58,16 @@ namespace :test do
break if system("lsof -i :4567", 1 => File::NULL)
if Time.now - start_time > 5
- puts "Timed out after 5 seconds"
+ puts "Failed to start puma after 5 seconds"
+ puts
+ puts File.read("log/test.log")
exit 1
end
+
+ sleep 0.2
end
- system("npm run lint && bundle exec ruby ../ci/qunit-selenium-runner.rb http://localhost:4567/")
+ system(*runner_command)
status = $?.exitstatus
ensure
Process.kill("KILL", -pid) if pid
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
index c4a614fa6d..8cb4648a67 100644
--- a/actionview/lib/action_view.rb
+++ b/actionview/lib/action_view.rb
@@ -35,7 +35,6 @@ module ActionView
eager_autoload do
autoload :Base
autoload :Context
- autoload :CompiledTemplates, "action_view/context"
autoload :Digestor
autoload :Helpers
autoload :LookupContext
@@ -45,6 +44,7 @@ module ActionView
autoload :Rendering
autoload :RoutingUrlFor
autoload :Template
+ autoload :FileTemplate
autoload :ViewPaths
autoload_under "renderer" do
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
index 6cb342a0a9..420136d6de 100644
--- a/actionview/lib/action_view/base.rb
+++ b/actionview/lib/action_view/base.rb
@@ -3,6 +3,7 @@
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"
@@ -10,10 +11,6 @@ require "action_view/template"
require "action_view/lookup_context"
module ActionView #:nodoc:
- module CompiledTemplates #:nodoc:
- # holds compiled template code
- end
-
# = Action View Base
#
# Action View templates can be written in several ways.
@@ -145,8 +142,6 @@ module ActionView #:nodoc:
class Base
include Helpers, ::ERB::Util, Context
- include CompiledTemplates
-
# Specify the proc used to decorate input tags that refer to attributes with errors.
cattr_accessor :field_error_proc, default: Proc.new { |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
@@ -185,9 +180,23 @@ module ActionView #:nodoc:
def xss_safe? #:nodoc:
true
end
+
+ def with_empty_template_cache # :nodoc:
+ subclass = Class.new(self) {
+ # We can't implement these as self.class because subclasses will
+ # share the same template cache as superclasses, so "changed?" won't work
+ # correctly.
+ define_method(:compiled_method_container) { subclass }
+ define_singleton_method(:compiled_method_container) { subclass }
+ }
+ end
+
+ def changed?(other) # :nodoc:
+ compiled_method_container != other.compiled_method_container
+ end
end
- attr_accessor :view_renderer
+ attr_reader :view_renderer
attr_internal :config, :assigns
delegate :lookup_context, to: :view_renderer
@@ -197,17 +206,51 @@ module ActionView #:nodoc:
@_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
end
- def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc:
+ # :stopdoc:
+
+ def self.build_renderer(context, controller, formats)
+ lookup_context = context.is_a?(ActionView::LookupContext) ?
+ context : ActionView::LookupContext.new(context)
+ lookup_context.formats = formats if formats
+ lookup_context.prefixes = controller._prefixes if controller
+ ActionView::Renderer.new(lookup_context)
+ end
+
+ def self.empty
+ with_view_paths([])
+ end
+
+ def self.with_view_paths(view_paths, assigns = {}, controller = nil)
+ with_context ActionView::LookupContext.new(view_paths), assigns, controller
+ end
+
+ def self.with_context(context, assigns = {}, controller = nil)
+ new ActionView::Renderer.new(context), assigns, controller
+ end
+
+ NULL = Object.new
+
+ # :startdoc:
+
+ def initialize(renderer = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc:
@_config = ActiveSupport::InheritableOptions.new
- if context.is_a?(ActionView::Renderer)
- @view_renderer = context
+ if formats == NULL
+ formats = nil
+ else
+ ActiveSupport::Deprecation.warn <<~eowarn
+ Passing formats to ActionView::Base.new is deprecated
+ eowarn
+ end
+
+ if renderer.is_a?(ActionView::Renderer)
+ @view_renderer = renderer
else
- lookup_context = context.is_a?(ActionView::LookupContext) ?
- context : ActionView::LookupContext.new(context)
- lookup_context.formats = formats if formats
- lookup_context.prefixes = controller._prefixes if controller
- @view_renderer = ActionView::Renderer.new(lookup_context)
+ ActiveSupport::Deprecation.warn <<~eowarn
+ ActionView::Base instances should be constructed with a view renderer,
+ assigments, and a controller.
+ eowarn
+ @view_renderer = self.class.build_renderer(renderer, controller, formats)
end
@cache_hit = {}
@@ -225,7 +268,11 @@ module ActionView #:nodoc:
end
def compiled_method_container
- CompiledTemplates
+ raise NotImplementedError, <<~msg
+ Subclasses of ActionView::Base must implement `compiled_method_container`
+ or use the class method `with_empty_template_cache` for constructing
+ an ActionView::Base subclass that has an empty cache.
+ msg
end
ActiveSupport.run_load_hooks(:action_view, self)
diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb
new file mode 100644
index 0000000000..4921074383
--- /dev/null
+++ b/actionview/lib/action_view/file_template.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require "action_view/template"
+
+module ActionView
+ class FileTemplate < Template
+ def initialize(filename, handler, details)
+ @filename = filename
+
+ super(nil, filename, handler, details)
+ end
+
+ def source
+ File.binread @filename
+ end
+
+ def refresh(_)
+ self
+ end
+
+ # Exceptions are marshalled when using the parallel test runner with DRb, so we need
+ # to ensure that references to the template object can be marshalled as well. This means forgoing
+ # the marshalling of the compiler mutex and instantiating that again on unmarshalling.
+ def marshal_dump # :nodoc:
+ [ @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants ]
+ end
+
+ def marshal_load(array) # :nodoc:
+ @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants = *array
+ @compile_mutex = Mutex.new
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
index c186cb8422..59d70a1dc4 100644
--- a/actionview/lib/action_view/helpers/asset_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -329,14 +329,14 @@ module ActionView
# image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw")
# # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw">
#
- # Active Storage (images that are uploaded by the users of your app):
+ # Active Storage blobs (images that are uploaded by the users of your app):
#
# image_tag(user.avatar)
# # => <img src="/rails/active_storage/blobs/.../tiger.jpg" />
- # image_tag(user.avatar.variant(resize_to_fit: [100, 100]))
- # # => <img src="/rails/active_storage/variants/.../tiger.jpg" />
- # image_tag(user.avatar.variant(resize_to_fit: [100, 100]), size: '100')
- # # => <img width="100" height="100" src="/rails/active_storage/variants/.../tiger.jpg" />
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]))
+ # # => <img src="/rails/active_storage/representations/.../tiger.jpg" />
+ # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100')
+ # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" />
def image_tag(source, options = {})
options = options.symbolize_keys
check_for_image_tag_errors(options)
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
index eef527d36f..0adecf362a 100644
--- a/actionview/lib/action_view/helpers/tags/base.rb
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -138,7 +138,7 @@ module ActionView
end
def sanitized_value(value)
- value.to_s.gsub(/\s/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s
+ value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s
end
def select_content_tag(option_tags, options, html_options)
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
index ae1c93e12f..67c86592b9 100644
--- a/actionview/lib/action_view/helpers/translation_helper.rb
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -138,7 +138,7 @@ module ActionView
end
def html_safe_translation_key?(key)
- /(\b|_|\.)html$/.match?(key.to_s)
+ /([_.]|\b)html\z/.match?(key.to_s)
end
end
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 554d223c0e..61fe11cf45 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -3,6 +3,7 @@
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
@@ -57,21 +58,36 @@ module ActionView
alias :eql? :equal?
@details_keys = Concurrent::Map.new
+ @digest_cache = Concurrent::Map.new
- def self.get(details)
+ def self.digest_cache(details)
+ @digest_cache[details_cache_key(details)] ||= Concurrent::Map.new
+ end
+
+ def self.details_cache_key(details)
if details[:formats]
details = details.dup
details[:formats] &= Template::Types.symbols
end
- @details_keys[details] ||= Concurrent::Map.new
+ @details_keys[details] ||= Object.new
end
def self.clear
+ ActionView::ViewPaths.all_view_paths.each do |path_set|
+ path_set.each(&:clear_cache)
+ end
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
+ @view_context_class = nil
@details_keys.clear
+ @digest_cache.clear
end
def self.digest_caches
- @details_keys.values
+ @digest_cache.values
+ end
+
+ def self.view_context_class(klass)
+ @view_context_class ||= klass.with_empty_template_cache
end
end
@@ -82,7 +98,7 @@ module ActionView
# Calculate the details key. Remove the handlers from calculation to improve performance
# since the user cannot modify it explicitly.
def details_key #:nodoc:
- @details_key ||= DetailsKey.get(@details) if @cache
+ @details_key ||= DetailsKey.details_cache_key(@details) if @cache
end
# Temporary skip passing the details_key forward.
@@ -96,7 +112,8 @@ module ActionView
private
def _set_detail(key, value) # :doc:
- @details = @details.dup if @details_key
+ @details = @details.dup if @digest_cache || @details_key
+ @digest_cache = nil
@details_key = nil
@details[key] = value
end
@@ -106,12 +123,6 @@ module ActionView
module ViewPaths
attr_reader :view_paths, :html_fallback_for_js
- # Whenever setting view paths, makes a copy so that we can manipulate them in
- # instance objects as we wish.
- def view_paths=(paths)
- @view_paths = ActionView::PathSet.new(Array(paths))
- end
-
def find(name, prefixes = [], partial = false, keys = [], options = {})
@view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
end
@@ -138,19 +149,34 @@ module ActionView
# Adds fallbacks to the view paths. Useful in cases when you are rendering
# a :file.
def with_fallbacks
- added_resolvers = 0
- self.class.fallbacks.each do |resolver|
- next if view_paths.include?(resolver)
- view_paths.push(resolver)
- added_resolvers += 1
+ view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq)
+
+ if block_given?
+ ActiveSupport::Deprecation.warn <<~eowarn
+ Calling `with_fallbacks` with a block is deprecated. Call methods on
+ the lookup context returned by `with_fallbacks` instead.
+ eowarn
+
+ begin
+ _view_paths = @view_paths
+ @view_paths = view_paths
+ yield
+ ensure
+ @view_paths = _view_paths
+ end
+ else
+ ActionView::LookupContext.new(view_paths, @details, @prefixes)
end
- yield
- ensure
- added_resolvers.times { view_paths.pop }
end
private
+ # Whenever setting view paths, makes a copy so that we can manipulate them in
+ # instance objects as we wish.
+ def build_view_paths(paths)
+ ActionView::PathSet.new(Array(paths))
+ end
+
def args_for_lookup(name, prefixes, partial, keys, details_options)
name, prefixes = normalize_name(name, prefixes)
details, details_key = detail_args_for(details_options)
@@ -163,7 +189,7 @@ module ActionView
user_details = @details.merge(options)
if @cache
- details_key = DetailsKey.get(user_details)
+ details_key = DetailsKey.details_cache_key(user_details)
else
details_key = nil
end
@@ -190,7 +216,7 @@ module ActionView
end
if @cache
- [details, DetailsKey.get(details)]
+ [details, DetailsKey.details_cache_key(details)]
else
[details, nil]
end
@@ -221,16 +247,17 @@ module ActionView
def initialize(view_paths, details = {}, prefixes = [])
@details_key = nil
+ @digest_cache = nil
@cache = true
@prefixes = prefixes
@rendered_format = nil
@details = initialize_details({}, details)
- self.view_paths = view_paths
+ @view_paths = build_view_paths(view_paths)
end
def digest_cache
- details_key
+ @digest_cache ||= DetailsKey.digest_cache(@details)
end
def initialize_details(target, details)
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index 12d06bf376..a25e1d3d02 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -6,11 +6,13 @@ require "rails"
module ActionView
# = Action View Railtie
class Railtie < Rails::Engine # :nodoc:
+ NULL_OPTION = Object.new
+
config.action_view = ActiveSupport::OrderedOptions.new
config.action_view.embed_authenticity_token_in_remote_forms = nil
config.action_view.debug_missing_translation = true
config.action_view.default_enforce_utf8 = nil
- config.action_view.finalize_compiled_template_methods = true
+ config.action_view.finalize_compiled_template_methods = NULL_OPTION
config.eager_load_namespaces << ActionView
@@ -48,8 +50,11 @@ module ActionView
initializer "action_view.finalize_compiled_template_methods" do |app|
ActiveSupport.on_load(:action_view) do
- ActionView::Template.finalize_compiled_template_methods =
- app.config.action_view.delete(:finalize_compiled_template_methods)
+ option = app.config.action_view.delete(:finalize_compiled_template_methods)
+
+ if option != NULL_OPTION
+ ActiveSupport::Deprecation.warn "action_view.finalize_compiled_template_methods is deprecated and has no effect"
+ end
end
end
diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
index 20b2523cac..ae366ce54a 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -17,7 +17,7 @@ module ActionView
# that new object is called in turn. This abstracts the setup and rendering
# into a separate classes for partials and templates.
class AbstractRenderer #:nodoc:
- delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context
+ delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
@@ -38,8 +38,6 @@ module ActionView
end
def instrument(name, **options) # :doc:
- options[:identifier] ||= (@template && @template.identifier) || @path
-
ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
yield payload
end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index cb850d75ee..801916954f 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -295,43 +295,57 @@ module ActionView
end
def render(context, options, block)
- setup(context, options, block)
- @template = find_partial
+ as = as_variable(options)
+ setup(context, options, as, block)
+
+ if @path
+ if @has_object || @collection
+ @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
+ @template_keys = retrieve_template_keys(@variable)
+ else
+ @template_keys = @locals.keys
+ end
+ template = find_partial(@path, @template_keys)
+ @variable ||= template.variable
+ else
+ template = nil
+ end
@lookup_context.rendered_format ||= begin
- if @template && @template.formats.present?
- @template.formats.first
+ if template && template.formats.first
+ template.formats.first
else
formats.first
end
end
if @collection
- render_collection
+ render_collection(context, template)
else
- render_partial
+ render_partial(context, template)
end
end
private
- def render_collection
- instrument(:collection, count: @collection.size) do |payload|
+ def render_collection(view, template)
+ identifier = (template && template.identifier) || @path
+ instrument(:collection, identifier: identifier, count: @collection.size) do |payload|
return nil if @collection.blank?
if @options.key?(:spacer_template)
- spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
+ spacer = find_template(@options[:spacer_template], @locals.keys).render(view, @locals)
end
- cache_collection_render(payload) do
- @template ? collection_with_template : collection_without_template
+ cache_collection_render(payload, view, template) do
+ template ? collection_with_template(view, template) : collection_without_template(view)
end.join(spacer).html_safe
end
end
- def render_partial
- instrument(:partial) do |payload|
- view, locals, block = @view, @locals, @block
+ def render_partial(view, template)
+ instrument(:partial, identifier: template.identifier) do |payload|
+ locals, block = @locals, @block
object, as = @object, @variable
if !block && (layout = @options[:layout])
@@ -341,12 +355,12 @@ module ActionView
object = locals[as] if object.nil? # Respect object when object is false
locals[as] = object if @has_object
- content = @template.render(view, locals) do |*name|
+ content = template.render(view, locals) do |*name|
view._layout_for(*name, &block)
end
content = layout.render(view, locals) { content } if layout
- payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path]
+ payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path]
content
end
end
@@ -358,12 +372,11 @@ module ActionView
# If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is
# set to that string. Otherwise, the +options[:partial]+ object must
# respond to +to_partial_path+ in order to setup the path.
- def setup(context, options, block)
- @view = context
+ def setup(context, options, as, block)
@options = options
@block = block
- @locals = options[:locals] ? options[:locals].symbolize_keys : {}
+ @locals = options[:locals] || {}
@details = extract_details(options)
prepend_formats(options[:formats])
@@ -381,26 +394,26 @@ module ActionView
@collection = collection_from_object || collection_from_options
if @collection
- paths = @collection_data = @collection.map { |o| partial_path(o) }
- @path = paths.uniq.one? ? paths.first : nil
+ paths = @collection_data = @collection.map { |o| partial_path(o, context) }
+ if paths.uniq.length == 1
+ @path = paths.first
+ else
+ paths.map! { |path| retrieve_variable(path, as).unshift(path) }
+ @path = nil
+ end
else
- @path = partial_path
+ @path = partial_path(@object, context)
end
end
+ self
+ end
+
+ def as_variable(options)
if as = options[:as]
raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s)
- as = as.to_sym
+ as.to_sym
end
-
- if @path
- @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as)
- @template_keys = retrieve_template_keys
- else
- paths.map! { |path| retrieve_variable(path, as).unshift(path) }
- end
-
- self
end
def collection_from_options
@@ -414,8 +427,8 @@ module ActionView
@object.to_ary if @object.respond_to?(:to_ary)
end
- def find_partial
- find_template(@path, @template_keys) if @path
+ def find_partial(path, template_keys)
+ find_template(path, template_keys)
end
def find_template(path, locals)
@@ -423,8 +436,8 @@ module ActionView
@lookup_context.find_template(path, prefixes, true, locals, @details)
end
- def collection_with_template
- view, locals, template = @view, @locals, @template
+ def collection_with_template(view, template)
+ locals = @locals
as, counter, iteration = @variable, @variable_counter, @variable_iteration
if layout = @options[:layout]
@@ -445,8 +458,8 @@ module ActionView
end
end
- def collection_without_template
- view, locals, collection_data = @view, @locals, @collection_data
+ def collection_without_template(view)
+ locals, collection_data = @locals, @collection_data
cache = {}
keys = @locals.keys
@@ -474,7 +487,7 @@ module ActionView
#
# If +prefix_partial_path_with_controller_namespace+ is true, then this
# method will prefix the partial paths with a namespace.
- def partial_path(object = @object)
+ def partial_path(object, view)
object = object.to_model if object.respond_to?(:to_model)
path = if object.respond_to?(:to_partial_path)
@@ -483,7 +496,7 @@ module ActionView
raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
end
- if @view.prefix_partial_path_with_controller_namespace
+ if view.prefix_partial_path_with_controller_namespace
prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
else
path
@@ -511,9 +524,9 @@ module ActionView
end
end
- def retrieve_template_keys
+ def retrieve_template_keys(variable)
keys = @locals.keys
- keys << @variable if @has_object || @collection
+ keys << variable
if @collection
keys << @variable_counter
keys << @variable_iteration
diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
index 5aa6f77902..388f9e5e56 100644
--- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -11,13 +11,13 @@ module ActionView
end
private
- def cache_collection_render(instrumentation_payload)
+ def cache_collection_render(instrumentation_payload, view, template)
return yield unless @options[:cached]
# Result is a hash with the key represents the
# key used for cache lookup and the value is the item
# on which the partial is being rendered
- keyed_collection = collection_by_cache_keys
+ keyed_collection = collection_by_cache_keys(view, template)
# Pull all partials from cache
# Result is a hash, key matches the entry in
@@ -51,23 +51,21 @@ module ActionView
@options[:cached].respond_to?(:call)
end
- def collection_by_cache_keys
+ def collection_by_cache_keys(view, template)
seed = callable_cache_key? ? @options[:cached] : ->(i) { i }
+ digest_path = view.digest_path_from_virtual(template.virtual_path)
+
@collection.each_with_object({}) do |item, hash|
- hash[expanded_cache_key(seed.call(item))] = item
+ hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item
end
end
- def expanded_cache_key(key)
- key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path, digest_path: digest_path))
+ def expanded_cache_key(key, view, template, digest_path)
+ key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path))
key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0.
end
- def digest_path
- @digest_path ||= @view.digest_path_from_virtual(@template.virtual_path)
- end
-
# `order_by` is an enumerable object containing keys of the cache,
# all keys are passed in whether found already or not.
#
diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index bb9db21e32..f414620923 100644
--- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -43,14 +43,14 @@ module ActionView
# For streaming, instead of rendering a given a template, we return a Body
# object that responds to each. This object is initialized with a block
# that knows how to render the template.
- def render_template(template, layout_name = nil, locals = {}) #:nodoc:
+ def render_template(view, template, layout_name = nil, locals = {}) #:nodoc:
return [super] unless layout_name && template.supports_streaming?
locals ||= {}
layout = layout_name && find_layout(layout_name, locals.keys, [formats.first])
Body.new do |buffer|
- delayed_render(buffer, template, layout, @view, locals)
+ delayed_render(buffer, template, layout, view, locals)
end
end
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
index ce8908924a..c36baeffcd 100644
--- a/actionview/lib/action_view/renderer/template_renderer.rb
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -5,7 +5,6 @@ require "active_support/core_ext/object/try"
module ActionView
class TemplateRenderer < AbstractRenderer #:nodoc:
def render(context, options)
- @view = context
@details = extract_details(options)
template = determine_template(options)
@@ -13,7 +12,7 @@ module ActionView
@lookup_context.rendered_format ||= (template.formats.first || formats.first)
- render_template(template, options[:layout], options[:locals])
+ render_template(context, template, options[:layout], options[:locals] || {})
end
private
@@ -29,7 +28,7 @@ module ActionView
elsif options.key?(:html)
Template::HTML.new(options[:html], formats.first)
elsif options.key?(:file)
- with_fallbacks { find_file(options[:file], nil, false, keys, @details) }
+ @lookup_context.with_fallbacks.find_file(options[:file], nil, false, keys, @details)
elsif options.key?(:inline)
handler = Template.handler_for_extension(options[:type] || "erb")
Template.new(options[:inline], "inline template", handler, locals: keys)
@@ -37,7 +36,7 @@ module ActionView
if options[:template].respond_to?(:render)
options[:template]
else
- find_template(options[:template], options[:prefixes], false, keys, @details)
+ @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
end
else
raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option."
@@ -46,22 +45,19 @@ module ActionView
# Renders the given template. A string representing the layout can be
# supplied as well.
- def render_template(template, layout_name = nil, locals = nil)
- view, locals = @view, locals || {}
-
- render_with_layout(layout_name, locals) do |layout|
+ def render_template(view, template, layout_name, locals)
+ render_with_layout(view, layout_name, locals) do |layout|
instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do
template.render(view, locals) { |*name| view._layout_for(*name) }
end
end
end
- def render_with_layout(path, locals)
+ def render_with_layout(view, path, locals)
layout = path && find_layout(path, locals.keys, [formats.first])
content = yield(layout)
if layout
- view = @view
view.view_flow.set(:layout, content)
layout.render(view, locals) { |*name| view._layout_for(*name) }
else
@@ -84,9 +80,9 @@ module ActionView
when String
begin
if layout.start_with?("/")
- with_fallbacks { find_template(layout, nil, false, [], details) }
+ @lookup_context.with_fallbacks.find_template(layout, nil, false, [], details)
else
- find_template(layout, nil, false, [], details)
+ @lookup_context.find_template(layout, nil, false, [], details)
end
rescue ActionView::MissingTemplate
all_details = @details.merge(formats: @lookup_context.default_formats)
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
index 205665a8c6..da92ce1f5e 100644
--- a/actionview/lib/action_view/rendering.rb
+++ b/actionview/lib/action_view/rendering.rb
@@ -35,24 +35,36 @@ module ActionView
end
module ClassMethods
- def view_context_class
- @view_context_class ||= begin
- supports_path = supports_path?
- routes = respond_to?(:_routes) && _routes
- helpers = respond_to?(:_helpers) && _helpers
-
- Class.new(ActionView::Base) do
- if routes
- include routes.url_helpers(supports_path)
- include routes.mounted_helpers
- end
-
- if helpers
- include helpers
- end
+ def _routes
+ end
+
+ def _helpers
+ end
+
+ def build_view_context_class(klass, supports_path, routes, helpers)
+ Class.new(klass) do
+ if routes
+ include routes.url_helpers(supports_path)
+ include routes.mounted_helpers
+ end
+
+ if helpers
+ include helpers
end
end
end
+
+ def view_context_class
+ klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base)
+
+ @view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers)
+
+ if klass.changed?(@view_context_class)
+ @view_context_class = build_view_context_class(klass, supports_path?, _routes, _helpers)
+ end
+
+ @view_context_class
+ end
end
def view_context_class
@@ -75,6 +87,7 @@ module ActionView
# Returns an object that is able to render templates.
def view_renderer # :nodoc:
+ # Lifespan: Per controller
@_view_renderer ||= ActionView::Renderer.new(lookup_context)
end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 8ce8053011..37f476e554 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -2,14 +2,22 @@
require "active_support/core_ext/object/try"
require "active_support/core_ext/kernel/singleton_class"
+require "active_support/deprecation"
require "thread"
+require "delegate"
module ActionView
# = Action View Template
class Template
extend ActiveSupport::Autoload
- mattr_accessor :finalize_compiled_template_methods, default: true
+ def self.finalize_compiled_template_methods
+ ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods is deprecated and has no effect"
+ end
+
+ def self.finalize_compiled_template_methods=(_)
+ ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods= is deprecated and has no effect"
+ end
# === Encodings in ActionView::Template
#
@@ -117,15 +125,7 @@ module ActionView
attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
- # This finalizer is needed (and exactly with a proc inside another proc)
- # otherwise templates leak in development.
- Finalizer = proc do |method_name, mod| # :nodoc:
- proc do
- mod.module_eval do
- remove_possible_method method_name
- end
- end
- end
+ attr_reader :variable
def initialize(source, identifier, handler, details)
format = details[:format] || (handler.default_format if handler.respond_to?(:default_format))
@@ -137,6 +137,13 @@ module ActionView
@original_encoding = nil
@locals = details[:locals] || []
@virtual_path = details[:virtual_path]
+
+ @variable = if @virtual_path
+ base = @virtual_path[-1] == "/" ? "" : File.basename(@virtual_path)
+ base =~ /\A_?(.*?)(?:\.\w+)*\z/
+ $1.to_sym
+ end
+
@updated_at = details[:updated_at] || Time.now
@formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f }
@variants = [details[:variant]]
@@ -202,7 +209,9 @@ module ActionView
# before passing the source on to the template engine, leaving a
# blank line in its stead.
def encode!
- return unless source.encoding == Encoding::BINARY
+ source = self.source
+
+ return source unless source.encoding == Encoding::BINARY
# Look for # encoding: *. If we find one, we'll encode the
# String in that encoding, otherwise, we'll use the
@@ -277,6 +286,15 @@ module ActionView
end
end
+ class LegacyTemplate < DelegateClass(Template) # :nodoc:
+ attr_reader :source
+
+ def initialize(template, source)
+ super(template)
+ @source = source
+ end
+ end
+
# Among other things, this method is responsible for properly setting
# the encoding of the compiled template.
#
@@ -290,8 +308,8 @@ module ActionView
# In general, this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding.
def compile(mod)
- encode!
- code = @handler.call(self)
+ source = encode!
+ code = @handler.call(self, source)
# Make sure that the resulting String to be eval'd is in the
# encoding of the code
@@ -312,13 +330,10 @@ module ActionView
# handler is valid in the default_internal. This is for handlers
# that handle encoding but screw up
unless source.valid_encoding?
- raise WrongEncodingError.new(@source, Encoding.default_internal)
+ raise WrongEncodingError.new(source, Encoding.default_internal)
end
mod.module_eval(source, identifier, 0)
- if finalize_compiled_template_methods
- ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
- end
end
def handle_render_error(view, e)
diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index 7ec76dcc3f..ddaac7a100 100644
--- a/actionview/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "active_support/deprecation"
+
module ActionView #:nodoc:
# = Action View Template Handlers
class Template #:nodoc:
@@ -14,7 +16,7 @@ module ActionView #:nodoc:
base.register_template_handler :erb, ERB.new
base.register_template_handler :html, Html.new
base.register_template_handler :builder, Builder.new
- base.register_template_handler :ruby, :source.to_proc
+ base.register_template_handler :ruby, lambda { |_, source| source }
end
@@template_handlers = {}
@@ -24,11 +26,35 @@ module ActionView #:nodoc:
@@template_extensions ||= @@template_handlers.keys
end
+ class LegacyHandlerWrapper < SimpleDelegator # :nodoc:
+ def call(view, source)
+ __getobj__.call(ActionView::Template::LegacyTemplate.new(view, source))
+ end
+ end
+
# Register an object that knows how to handle template files with the given
# extensions. This can be used to implement new template types.
# The handler must respond to +:call+, which will be passed the template
# and should return the rendered template as a String.
def register_template_handler(*extensions, handler)
+ params = if handler.is_a?(Proc)
+ handler.parameters
+ else
+ handler.method(:call).parameters
+ end
+
+ unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2
+ ActiveSupport::Deprecation.warn <<~eowarn
+ Single arity template handlers are deprecated. Template handlers must
+ now accept two parameters, the view object and the source for the view object.
+ Change:
+ >> #{handler.class}#call(#{params.map(&:last).join(", ")})
+ To:
+ >> #{handler.class}#call(#{params.map(&:last).join(", ")}, source)
+ eowarn
+ handler = LegacyHandlerWrapper.new(handler)
+ end
+
raise(ArgumentError, "Extension is required") if extensions.empty?
extensions.each do |extension|
@@template_handlers[extension.to_sym] = handler
diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb
index 61492ce448..e5413cd2a0 100644
--- a/actionview/lib/action_view/template/handlers/builder.rb
+++ b/actionview/lib/action_view/template/handlers/builder.rb
@@ -5,11 +5,11 @@ module ActionView
class Builder
class_attribute :default_format, default: :xml
- def call(template)
+ def call(template, source)
require_engine
"xml = ::Builder::XmlMarkup.new(:indent => 2);" \
"self.output_buffer = xml.target!;" +
- template.source +
+ source +
";xml.target!;"
end
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 7d1a6767d7..b6314a5bc3 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -28,8 +28,8 @@ module ActionView
ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
- def self.call(template)
- new.call(template)
+ def self.call(template, source)
+ new.call(template, source)
end
def supports_streaming?
@@ -40,17 +40,17 @@ module ActionView
true
end
- def call(template)
+ def call(template, source)
# First, convert to BINARY, so in case the encoding is
# wrong, we can still find an encoding tag
# (<%# encoding %>) inside the String using a regular
# expression
- template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
+ template_source = source.dup.force_encoding(Encoding::ASCII_8BIT)
erb = template_source.gsub(ENCODING_TAG, "")
encoding = $2
- erb.force_encoding valid_encoding(template.source.dup, encoding)
+ erb.force_encoding valid_encoding(source.dup, encoding)
# Always make sure we return a String in the default_internal
erb.encode!
diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb
index e155bae89d..15ca202024 100644
--- a/actionview/lib/action_view/template/handlers/erb/erubi.rb
+++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb
@@ -26,7 +26,7 @@ module ActionView
view = Class.new(ActionView::Base) {
include action_view_erb_handler_context._routes.url_helpers
class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0)
- }.new(action_view_erb_handler_context)
+ }.with_context(action_view_erb_handler_context)
view.run(:_template, {}, ActionView::OutputBuffer.new)
end
diff --git a/actionview/lib/action_view/template/handlers/html.rb b/actionview/lib/action_view/template/handlers/html.rb
index 27004a318c..65857d8587 100644
--- a/actionview/lib/action_view/template/handlers/html.rb
+++ b/actionview/lib/action_view/template/handlers/html.rb
@@ -3,7 +3,7 @@
module ActionView
module Template::Handlers
class Html < Raw
- def call(template)
+ def call(template, source)
"ActionView::OutputBuffer.new #{super}"
end
end
diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb
index 5cd23a0060..57ebb169fc 100644
--- a/actionview/lib/action_view/template/handlers/raw.rb
+++ b/actionview/lib/action_view/template/handlers/raw.rb
@@ -3,8 +3,8 @@
module ActionView
module Template::Handlers
class Raw
- def call(template)
- "#{template.source.inspect}.html_safe;"
+ def call(template, source)
+ "#{source.inspect}.html_safe;"
end
end
end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 12ae82f8c5..3b4594942b 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -226,9 +226,8 @@ module ActionView
template_paths.map do |template|
handler, format, variant = extract_handler_and_format_and_variant(template)
- contents = File.binread(template)
- Template.new(contents, File.expand_path(template), handler,
+ FileTemplate.new(File.expand_path(template), handler,
virtual_path: path.virtual,
format: format,
variant: variant,
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index 1fad08a689..d6203b95c5 100644
--- a/actionview/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -8,13 +8,15 @@ module ActionView #:nodoc:
# useful for testing extensions that have no way of knowing what the file
# system will look like at runtime.
class FixtureResolver < PathResolver
- attr_reader :hash
-
def initialize(hash = {}, pattern = nil)
super(pattern)
@hash = hash
end
+ def data
+ @hash
+ end
+
def to_s
@hash.keys.join(", ")
end
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index d5694d77f4..3ca5aedc14 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -5,13 +5,21 @@ module ActionView
extend ActiveSupport::Concern
included do
- class_attribute :_view_paths, default: ActionView::PathSet.new.freeze
+ ViewPaths.set_view_paths(self, ActionView::PathSet.new.freeze)
end
delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
:locale, :locale=, to: :lookup_context
module ClassMethods
+ def _view_paths
+ ViewPaths.get_view_paths(self)
+ end
+
+ def _view_paths=(paths)
+ ViewPaths.set_view_paths(self, paths)
+ end
+
def _prefixes # :nodoc:
@_prefixes ||= begin
return local_prefixes if superclass.abstract?
@@ -29,6 +37,22 @@ module ActionView
end
end
+ # :stopdoc:
+ @all_view_paths = {}
+
+ def self.get_view_paths(klass)
+ @all_view_paths[klass] || get_view_paths(klass.superclass)
+ end
+
+ def self.set_view_paths(klass, paths)
+ @all_view_paths[klass] = paths
+ end
+
+ def self.all_view_paths
+ @all_view_paths.values.uniq
+ end
+ # :startdoc:
+
# The prefixes used in render "foo" shortcuts.
def _prefixes # :nodoc:
self.class._prefixes
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index d71944e938..b649b3c9dd 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -48,7 +48,8 @@ module RenderERBUtils
@view ||= begin
path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
view_paths = ActionView::PathSet.new([path])
- ActionView::Base.new(view_paths)
+ view = ActionView::Base.with_empty_template_cache
+ view.with_view_paths(view_paths)
end
end
@@ -58,10 +59,11 @@ module RenderERBUtils
template = ActionView::Template.new(
string.strip,
"test template",
- ActionView::Template::Handlers::ERB,
+ ActionView::Template.handler_for_extension(:erb),
{})
- template.render(ActionView::Base.new, {}).strip
+ view = ActionView::Base.with_empty_template_cache
+ template.render(view.empty, {}).strip
end
end
diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
index 6d5c97b7fd..838b564c5d 100644
--- a/actionview/test/actionpack/controller/layout_test.rb
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -70,7 +70,7 @@ class LayoutAutoDiscoveryTest < ActionController::TestCase
end
def test_third_party_template_library_auto_discovers_layout
- with_template_handler :mab, lambda { |template| template.source.inspect } do
+ with_template_handler :mab, lambda { |template, source| source.inspect } do
@controller = ThirdPartyTemplateLibraryController.new
get :hello
assert_response :success
@@ -212,7 +212,7 @@ class LayoutSetInResponseTest < ActionController::TestCase
end
def test_layout_set_when_using_render
- with_template_handler :mab, lambda { |template| template.source.inspect } do
+ with_template_handler :mab, lambda { |template, source| source.inspect } do
@controller = SetsLayoutInRenderController.new
get :hello
assert_includes @response.body, "layouts/third_party_template_library.mab"
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
index 204903c60c..727d3fbc1a 100644
--- a/actionview/test/actionpack/controller/render_test.rb
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -485,8 +485,8 @@ class TestController < ActionController::Base
render partial: "customer", locals: { customer: Customer.new("david") }
end
- def partial_with_string_locals
- render partial: "customer", locals: { "customer" => Customer.new("david") }
+ def partial_with_hashlike_locals
+ render partial: "customer", locals: ActionController::Parameters.new(customer: Customer.new("david"))
end
def partial_with_form_builder
@@ -691,7 +691,7 @@ class RenderTest < ActionController::TestCase
get :partial_with_locals, to: "test#partial_with_locals"
get :partial_with_nested_object, to: "test#partial_with_nested_object"
get :partial_with_nested_object_shorthand, to: "test#partial_with_nested_object_shorthand"
- get :partial_with_string_locals, to: "test#partial_with_string_locals"
+ get :partial_with_hashlike_locals, to: "test#partial_with_hashlike_locals"
get :partials_list, to: "test#partials_list"
get :render_action_hello_world, to: "test#render_action_hello_world"
get :render_action_hello_world_as_string, to: "test#render_action_hello_world_as_string"
@@ -1292,8 +1292,8 @@ class RenderTest < ActionController::TestCase
assert_equal "Hello: david", @response.body
end
- def test_partial_with_string_locals
- get :partial_with_string_locals
+ def test_partial_with_hashlike_locals
+ get :partial_with_hashlike_locals
assert_equal "Hello: david", @response.body
end
diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb
index 7f3fe0fa08..c5238dd746 100644
--- a/actionview/test/actionpack/controller/view_paths_test.rb
+++ b/actionview/test/actionpack/controller/view_paths_test.rb
@@ -77,7 +77,7 @@ class ViewLoadPathsTest < ActionController::TestCase
end
def test_template_appends_view_path_correctly
- @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
+ @controller.instance_variable_set :@template, ActionView::Base.with_view_paths(TestController.view_paths, {}, @controller)
class_view_paths = TestController.view_paths
@controller.append_view_path "foo"
@@ -89,7 +89,7 @@ class ViewLoadPathsTest < ActionController::TestCase
end
def test_template_prepends_view_path_correctly
- @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
+ @controller.instance_variable_set :@template, ActionView::Base.with_view_paths(TestController.view_paths, {}, @controller)
class_view_paths = TestController.view_paths
@controller.prepend_view_path "baz"
diff --git a/actionview/test/activerecord/multifetch_cache_test.rb b/actionview/test/activerecord/multifetch_cache_test.rb
index 12be069e69..f56168bda5 100644
--- a/actionview/test/activerecord/multifetch_cache_test.rb
+++ b/actionview/test/activerecord/multifetch_cache_test.rb
@@ -10,8 +10,10 @@ class MultifetchCacheTest < ActiveRecordTestCase
def setup
view_paths = ActionController::Base.view_paths
+ view_paths.each(&:clear_cache)
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
- @view = Class.new(ActionView::Base) do
+ @view = Class.new(ActionView::Base.with_empty_template_cache) do
def view_cache_dependencies
[]
end
@@ -19,7 +21,7 @@ class MultifetchCacheTest < ActiveRecordTestCase
def combined_fragment_cache_key(key)
[ :views, key ]
end
- end.new(view_paths, {})
+ end.with_view_paths(view_paths, {})
end
def test_only_preloading_for_records_that_miss_the_cache
diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb
index e172497c88..45070674ad 100644
--- a/actionview/test/template/capture_helper_test.rb
+++ b/actionview/test/template/capture_helper_test.rb
@@ -5,7 +5,7 @@ require "abstract_unit"
class CaptureHelperTest < ActionView::TestCase
def setup
super
- @av = ActionView::Base.new
+ @av = ActionView::Base.empty
@view_flow = ActionView::OutputFlow.new
end
diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
index 3cd6448e38..6375555bb4 100644
--- a/actionview/test/template/compiled_templates_test.rb
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -3,7 +3,18 @@
require "abstract_unit"
class CompiledTemplatesTest < ActiveSupport::TestCase
- teardown do
+ attr_reader :view_class
+
+ def setup
+ super
+ view_paths = ActionController::Base.view_paths
+ view_paths.each(&:clear_cache)
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
+ @view_class = ActionView::Base.with_empty_template_cache
+ end
+
+ def teardown
+ super
ActionView::LookupContext::DetailsKey.clear
end
@@ -72,13 +83,13 @@ class CompiledTemplatesTest < ActiveSupport::TestCase
def render_with_cache(*args)
view_paths = ActionController::Base.view_paths
- ActionView::Base.new(view_paths, {}).render(*args)
+ view_class.with_view_paths(view_paths, {}).render(*args)
end
def render_without_cache(*args)
path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
view_paths = ActionView::PathSet.new([path])
- ActionView::Base.new(view_paths, {}).render(*args)
+ view_class.with_view_paths(view_paths, {}).render(*args)
end
def modify_template(template, content)
diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
index ef7aeac039..42cb14a18a 100644
--- a/actionview/test/template/dependency_tracker_test.rb
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -17,8 +17,8 @@ class FakeTemplate
end
end
-Neckbeard = lambda { |template| template.source }
-Bowtie = lambda { |template| template.source }
+Neckbeard = lambda { |template, source| source }
+Bowtie = lambda { |template, source| source }
class DependencyTrackerTest < ActionView::TestCase
def tracker
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index 6db55a1447..ca117d4a30 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -48,8 +48,16 @@ class FormCollectionsHelperTest < ActionView::TestCase
test "collection radio should sanitize collection values for labels correctly" do
with_collection_radio_buttons :user, :name, ["$0.99", "$1.99"], :to_s, :to_s
- assert_select "label[for=user_name_099]", "$0.99"
- assert_select "label[for=user_name_199]", "$1.99"
+ assert_select "label[for=user_name_0_99]", "$0.99"
+ assert_select "label[for=user_name_1_99]", "$1.99"
+ end
+
+ test "collection radio correctly builds unique DOM IDs for float values" do
+ with_collection_radio_buttons :user, :name, [1.0, 10], :to_s, :to_s
+ assert_select "label[for=user_name_1_0]", "1.0"
+ assert_select "label[for=user_name_10]", "10"
+ assert_select 'input#user_name_1_0[type=radio][value="1.0"]'
+ assert_select 'input#user_name_10[type=radio][value="10"]'
end
test "collection radio accepts checked item" do
@@ -302,8 +310,16 @@ class FormCollectionsHelperTest < ActionView::TestCase
test "collection check box should sanitize collection values for labels correctly" do
with_collection_check_boxes :user, :name, ["$0.99", "$1.99"], :to_s, :to_s
- assert_select "label[for=user_name_099]", "$0.99"
- assert_select "label[for=user_name_199]", "$1.99"
+ assert_select "label[for=user_name_0_99]", "$0.99"
+ assert_select "label[for=user_name_1_99]", "$1.99"
+ end
+
+ test "collection check boxes correctly builds unique DOM IDs for float values" do
+ with_collection_check_boxes :user, :name, [1.0, 10], :to_s, :to_s
+ assert_select "label[for=user_name_1_0]", "1.0"
+ assert_select "label[for=user_name_10]", "10"
+ assert_select 'input#user_name_1_0[type=checkbox][value="1.0"]'
+ assert_select 'input#user_name_10[type=checkbox][value="10"]'
end
test "collection check boxes generates labels for non-English values correctly" do
diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb
index 9fcf80bb24..83bb651ea3 100644
--- a/actionview/test/template/log_subscriber_test.rb
+++ b/actionview/test/template/log_subscriber_test.rb
@@ -11,10 +11,13 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
def setup
super
- view_paths = ActionController::Base.view_paths
+ ActionView::LookupContext::DetailsKey.clear
+
+ view_paths = ActionController::Base.view_paths
+
lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"])
renderer = ActionView::Renderer.new(lookup_context)
- @view = ActionView::Base.new(renderer, {})
+ @view = ActionView::Base.with_empty_template_cache.new(renderer, {})
ActionView::LogSubscriber.attach_to :action_view
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
index 68e151f154..290f832794 100644
--- a/actionview/test/template/lookup_context_test.rb
+++ b/actionview/test/template/lookup_context_test.rb
@@ -5,10 +5,14 @@ require "abstract_controller/rendering"
class LookupContextTest < ActiveSupport::TestCase
def setup
- @lookup_context = ActionView::LookupContext.new(FIXTURE_LOAD_PATH, {})
+ @lookup_context = build_lookup_context(FIXTURE_LOAD_PATH, {})
ActionView::LookupContext::DetailsKey.clear
end
+ def build_lookup_context(paths, details)
+ ActionView::LookupContext.new(paths, details)
+ end
+
def teardown
I18n.locale = :en
end
@@ -118,19 +122,32 @@ class LookupContextTest < ActiveSupport::TestCase
test "adds fallbacks to view paths when required" do
assert_equal 1, @lookup_context.view_paths.size
- @lookup_context.with_fallbacks do
- assert_equal 3, @lookup_context.view_paths.size
- assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
- assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
+ assert_deprecated do
+ @lookup_context.with_fallbacks do
+ assert_equal 3, @lookup_context.view_paths.size
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
+ end
end
+
+ @lookup_context = @lookup_context.with_fallbacks
+
+ assert_equal 3, @lookup_context.view_paths.size
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("")
+ assert_includes @lookup_context.view_paths, ActionView::FallbackFileSystemResolver.new("/")
end
test "add fallbacks just once in nested fallbacks calls" do
- @lookup_context.with_fallbacks do
+ assert_deprecated do
@lookup_context.with_fallbacks do
- assert_equal 3, @lookup_context.view_paths.size
+ @lookup_context.with_fallbacks do
+ assert_equal 3, @lookup_context.view_paths.size
+ end
end
end
+
+ @lookup_context = @lookup_context.with_fallbacks.with_fallbacks
+ assert_equal 3, @lookup_context.view_paths.size
end
test "generates a new details key for each details hash" do
@@ -156,13 +173,13 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "gives the key forward to the resolver, so it can be used as cache key" do
- @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo")
+ @lookup_context = build_lookup_context(ActionView::FixtureResolver.new("test/_foo.erb" => "Foo"), {})
template = @lookup_context.find("foo", %w(test), true)
assert_equal "Foo", template.source
# Now we are going to change the template, but it won't change the returned template
# since we will hit the cache.
- @lookup_context.view_paths.first.hash["test/_foo.erb"] = "Bar"
+ @lookup_context.view_paths.first.data["test/_foo.erb"] = "Bar"
template = @lookup_context.find("foo", %w(test), true)
assert_equal "Foo", template.source
@@ -185,7 +202,7 @@ class LookupContextTest < ActiveSupport::TestCase
end
test "can disable the cache on demand" do
- @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo")
+ @lookup_context = build_lookup_context(ActionView::FixtureResolver.new("test/_foo.erb" => "Foo"), {})
old_template = @lookup_context.find("foo", %w(test), true)
template = @lookup_context.find("foo", %w(test), true)
@@ -221,12 +238,12 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase
# Now we are going to change the template, but it won't change the returned template
# since the timestamp is the same.
- @resolver.hash["test/_foo.erb"][0] = "Bar"
+ @resolver.data["test/_foo.erb"][0] = "Bar"
template = @lookup_context.find("foo", %w(test), true)
assert_equal "Foo", template.source
# Now update the timestamp.
- @resolver.hash["test/_foo.erb"][1] = Time.now.utc
+ @resolver.data["test/_foo.erb"][1] = Time.now.utc
template = @lookup_context.find("foo", %w(test), true)
assert_equal "Bar", template.source
end
@@ -237,7 +254,7 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase
template = @lookup_context.find("foo", %w(test), true)
assert_equal "Foo", template.source
- @resolver.hash.clear
+ @resolver.data.clear
assert_raise ActionView::MissingTemplate do
@lookup_context.find("foo", %w(test), true)
end
@@ -246,12 +263,12 @@ class LookupContextWithFalseCaching < ActiveSupport::TestCase
test "if no template was cached in the first lookup, retrieval should work in the second call" do
ActionView::Resolver.stub(:caching?, false) do
- @resolver.hash.clear
+ @resolver.data.clear
assert_raise ActionView::MissingTemplate do
@lookup_context.find("foo", %w(test), true)
end
- @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)]
+ @resolver.data["test/_foo.erb"] = ["Foo", Time.utc(2000)]
template = @lookup_context.find("foo", %w(test), true)
assert_equal "Foo", template.source
end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index afe68b7ff0..3f298d81f3 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -9,15 +9,18 @@ end
module RenderTestCases
def setup_view(paths)
@assigns = { secret: "in the sauce" }
- @view = Class.new(ActionView::Base) do
+
+ @view = Class.new(ActionView::Base.with_empty_template_cache) do
def view_cache_dependencies; []; end
def combined_fragment_cache_key(key)
[ :views, key ]
end
- end.new(paths, @assigns)
+ end.with_view_paths(paths, @assigns)
+
+ controller = TestController.new
- @controller_view = TestController.new.view_context
+ @controller_view = controller.view_context_class.with_empty_template_cache.new(controller.view_renderer, controller.view_assigns, controller)
# Reload and register danish language for testing
I18n.backend.store_translations "da", {}
@@ -336,6 +339,16 @@ module RenderTestCases
assert_equal "Hello: davidHello: mary", @view.render(partial: "test/customer", collection: customers)
end
+ def test_deprecated_constructor
+ assert_deprecated do
+ ActionView::Base.new
+ end
+
+ assert_deprecated do
+ ActionView::Base.new ["/a"]
+ end
+ end
+
def test_render_partial_without_object_does_not_put_partial_name_to_local_assigns
assert_equal "false", @view.render(partial: "test/partial_name_in_local_assigns")
end
@@ -440,13 +453,31 @@ module RenderTestCases
assert_equal "Hello, World!", @view.render(inline: "Hello, World!", type: :bar)
end
- CustomHandler = lambda do |template|
+ CustomHandler = lambda do |template, source|
"@output_buffer = ''.dup\n" \
- "@output_buffer << 'source: #{template.source.inspect}'\n"
+ "@output_buffer << 'source: #{source.inspect}'\n"
end
def test_render_inline_with_render_from_to_proc
- ActionView::Template.register_template_handler :ruby_handler, :source.to_proc
+ ActionView::Template.register_template_handler :ruby_handler, lambda { |_, source| source }
+ assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
+ ensure
+ ActionView::Template.unregister_template_handler :ruby_handler
+ end
+
+ def test_render_inline_with_render_from_to_proc_deprecated
+ assert_deprecated do
+ ActionView::Template.register_template_handler :ruby_handler, :source.to_proc
+ end
+ assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
+ ensure
+ ActionView::Template.unregister_template_handler :ruby_handler
+ end
+
+ def test_optional_second_arg_works_without_deprecation
+ assert_not_deprecated do
+ ActionView::Template.register_template_handler :ruby_handler, ->(view, source = nil) { source }
+ end
assert_equal "3", @view.render(inline: "(1 + 2).to_s", type: :ruby_handler)
ensure
ActionView::Template.unregister_template_handler :ruby_handler
@@ -600,6 +631,7 @@ class CachedViewRenderTest < ActiveSupport::TestCase
# Ensure view path cache is primed
def setup
+ ActionView::LookupContext::DetailsKey.clear
view_paths = ActionController::Base.view_paths
assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class
setup_view(view_paths)
@@ -617,6 +649,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase
# Test the same thing as above, but make sure the view path
# is not eager loaded
def setup
+ ActionView::LookupContext::DetailsKey.clear
path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
view_paths = ActionView::PathSet.new([path])
assert_equal ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH), view_paths.first
@@ -674,6 +707,8 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase
# Ensure view path cache is primed
setup do
+ ActionView::LookupContext::DetailsKey.clear
+
view_paths = ActionController::Base.view_paths
assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class
diff --git a/actionview/test/template/resolver_cache_test.rb b/actionview/test/template/resolver_cache_test.rb
index 8a5db1346a..90b61a2aa1 100644
--- a/actionview/test/template/resolver_cache_test.rb
+++ b/actionview/test/template/resolver_cache_test.rb
@@ -4,6 +4,7 @@ require "abstract_unit"
class ResolverCacheTest < ActiveSupport::TestCase
def test_inspect_shields_cache_internals
+ ActionView::LookupContext::DetailsKey.clear
assert_match %r(#<ActionView::Resolver:0x[0-9a-f]+ @cache=#<ActionView::Resolver::Cache:0x[0-9a-f]+ keys=0 queries=0>>), ActionView::Resolver.new.inspect
end
end
diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb
index f196c42c4f..a5b59a700e 100644
--- a/actionview/test/template/streaming_render_test.rb
+++ b/actionview/test/template/streaming_render_test.rb
@@ -7,9 +7,12 @@ end
class SetupFiberedBase < ActiveSupport::TestCase
def setup
+ ActionView::LookupContext::DetailsKey.clear
+
view_paths = ActionController::Base.view_paths
+
@assigns = { secret: "in the sauce", name: nil }
- @view = ActionView::Base.new(view_paths, @assigns)
+ @view = ActionView::Base.with_empty_template_cache.with_view_paths(view_paths, @assigns)
@controller_view = TestController.new.view_context
end
diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb
index 8257d97b7c..a069c8f2d0 100644
--- a/actionview/test/template/template_test.rb
+++ b/actionview/test/template/template_test.rb
@@ -19,7 +19,8 @@ class TestERBTemplate < ActiveSupport::TestCase
end
class Context < ActionView::Base
- def initialize
+ def initialize(*)
+ super
@output_buffer = "original"
@virtual_path = nil
end
@@ -63,7 +64,8 @@ class TestERBTemplate < ActiveSupport::TestCase
end
def setup
- @context = Context.new
+ @context = Context.with_empty_template_cache.empty
+ super
end
def test_basic_template
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
index 976b6bc77e..c89aff9c9d 100644
--- a/actionview/test/template/test_case_test.rb
+++ b/actionview/test/template/test_case_test.rb
@@ -24,6 +24,11 @@ module ActionView
DeveloperStruct = Struct.new(:name)
module SharedTests
+ def setup
+ ActionView::LookupContext::DetailsKey.clear
+ super
+ end
+
def self.included(test_case)
test_case.class_eval do
test "helpers defined on ActionView::TestCase are available" do
@@ -52,7 +57,7 @@ module ActionView
end
test "retrieve non existing config values" do
- assert_nil ActionView::Base.new.config.something_odd
+ assert_nil ActionView::Base.empty.config.something_odd
end
test "works without testing a helper module" do
diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index e756348938..23fc9850c4 100644
--- a/actionview/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
@@ -36,7 +36,10 @@ class TranslationHelperTest < ActiveSupport::TestCase
}
}
)
- @view = ::ActionView::Base.new(ActionController::Base.view_paths, {})
+ view_paths = ActionController::Base.view_paths
+ view_paths.each(&:clear_cache)
+ ActionView::LookupContext.fallbacks.each(&:clear_cache)
+ @view = ::ActionView::Base.with_empty_template_cache.with_view_paths(view_paths, {})
end
teardown do
diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb
index 56f436c8b8..d7a6271587 100644
--- a/actionview/test/ujs/server.rb
+++ b/actionview/test/ujs/server.rb
@@ -23,6 +23,7 @@ module UJS
config.public_file_server.enabled = true
config.logger = Logger.new(STDOUT)
config.log_level = :error
+ config.hosts << proc { true }
config.content_security_policy do |policy|
policy.default_src :self, :https
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
index ce118c1e8a..5609d13f5f 100644
--- a/activejob/lib/active_job/enqueuing.rb
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -67,7 +67,7 @@ module ActiveJob
false
else
ActiveSupport::Deprecation.warn(
- "Rails 6.0 will return false when the enqueing is aborted. Make sure your code doesn't depend on it" \
+ "Rails 6.0 will return false when the enqueuing is aborted. Make sure your code doesn't depend on it" \
" returning the instance of the job and set `config.active_job.return_false_on_aborted_enqueue = true`" \
" to remove the deprecations."
)
diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb
index 7bb1e35181..de259261de 100644
--- a/activejob/lib/active_job/queue_name.rb
+++ b/activejob/lib/active_job/queue_name.rb
@@ -18,6 +18,26 @@ module ActiveJob
# post.to_feed!
# end
# end
+ #
+ # Can be given a block that will evaluate in the context of the job
+ # allowing +self.arguments+ to be accessed so that a dynamic queue name
+ # can be applied:
+ #
+ # class PublishToFeedJob < ApplicationJob
+ # queue_as do
+ # post = self.arguments.first
+ #
+ # if post.paid?
+ # :paid_feeds
+ # else
+ # :feeds
+ # end
+ # end
+ #
+ # def perform(post)
+ # post.to_feed!
+ # end
+ # end
def queue_as(part_name = nil, &block)
if block_given?
self.queue_name = block
diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb
index 046033921d..4d934df31b 100644
--- a/activejob/test/cases/test_helper_test.rb
+++ b/activejob/test/cases/test_helper_test.rb
@@ -1688,7 +1688,7 @@ class PerformedJobsTest < ActiveJob::TestCase
end
end
- def test_assert_performed_with_without_bllock_with_global_id_args
+ def test_assert_performed_with_without_block_with_global_id_args
ricardo = Person.new(9)
HelloJob.perform_later(ricardo)
perform_enqueued_jobs
diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb
index 1163ae8178..0c248dda01 100644
--- a/activejob/test/support/integration/adapters/backburner.rb
+++ b/activejob/test/support/integration/adapters/backburner.rb
@@ -4,6 +4,7 @@ module BackburnerJobsManager
def setup
ActiveJob::Base.queue_adapter = :backburner
Backburner.configure do |config|
+ config.beanstalk_url = ENV["BEANSTALK_URL"] if ENV["BEANSTALK_URL"]
config.logger = Rails.logger
end
unless can_run?
diff --git a/activejob/test/support/integration/adapters/resque.rb b/activejob/test/support/integration/adapters/resque.rb
index 2ed8302277..cd129e72b2 100644
--- a/activejob/test/support/integration/adapters/resque.rb
+++ b/activejob/test/support/integration/adapters/resque.rb
@@ -3,7 +3,7 @@
module ResqueJobsManager
def setup
ActiveJob::Base.queue_adapter = :resque
- Resque.redis = Redis::Namespace.new "active_jobs_int_test", redis: Redis.new(url: "redis://127.0.0.1:6379/12", thread_safe: true)
+ Resque.redis = Redis::Namespace.new "active_jobs_int_test", redis: Redis.new(url: ENV["REDIS_URL"] || "redis://127.0.0.1:6379/12", thread_safe: true)
Resque.logger = Rails.logger
unless can_run?
puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n"
diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb
index eb8d4cc2d5..89dc61ca28 100644
--- a/activejob/test/support/integration/adapters/sneakers.rb
+++ b/activejob/test/support/integration/adapters/sneakers.rb
@@ -7,7 +7,7 @@ module SneakersJobsManager
def setup
ActiveJob::Base.queue_adapter = :sneakers
Sneakers.configure heartbeat: 2,
- amqp: "amqp://guest:guest@localhost:5672",
+ amqp: ENV["RABBITMQ_URL"] || "amqp://guest:guest@localhost:5672",
vhost: "/",
exchange: "active_jobs_sneakers_int_test",
exchange_type: :direct,
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index d6c80b2c5d..02759b4ccb 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -32,7 +32,7 @@ module ActiveModel
value = options[key]
unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
- raise ArgumentError, ":#{key} must be a nonnegative Integer, Infinity, Symbol, or Proc"
+ raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
end
end
end
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 9cb8b543b0..51e224d5cd 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+require "bigdecimal/util"
+
module ActiveModel
module Validations
class NumericalityValidator < EachValidator # :nodoc:
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index fcdf123062..16c44762cb 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -281,7 +281,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
assert_predicate topic, :invalid?
end
- def test_validates_numericalty_with_object_acting_as_numeric
+ def test_validates_numericality_with_object_acting_as_numeric
klass = Class.new do
def to_f
123.54
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 2a0cd81be5..f16a746b91 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,57 @@
+* Fix `relation.create` to avoid leaking scope to initialization block and callbacks.
+
+ Fixes #9894, #17577.
+
+ *Ryuta Kamizono*
+
+* Chaining named scope is no longer leaking to class level querying methods.
+
+ Fixes #14003.
+
+ *Ryuta Kamizono*
+
+* Allow applications to automatically switch connections.
+
+ Adds a middleware and configuration options that can be used in your
+ application to automatically switch between the writing and reading
+ database connections.
+
+ `GET` and `HEAD` requests will read from the replica unless there was
+ a write in the last 2 seconds, otherwise they will read from the primary.
+ Non-get requests will always write to the primary. The middleware accepts
+ an argument for a Resolver class and a Operations class where you are able
+ to change how the auto-switcher works to be most beneficial for your
+ application.
+
+ To use the middleware in your application you can use the following
+ configuration options:
+
+ ```
+ config.active_record.database_selector = { delay: 2.seconds }
+ config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ ```
+
+ To change the database selection strategy, pass a custom class to the
+ configuration options:
+
+ ```
+ config.active_record.database_selector = { delay: 10.seconds }
+ config.active_record.database_resolver = MyResolver
+ config.active_record.database_resolver_context = MyResolver::MyCookies
+ ```
+
+ *Eileen M. Uchitelle*
+
+* MySQL: Support `:size` option to change text and blob size.
+
+ *Ryuta Kamizono*
+
+* Make `t.timestamps` with precision by default.
+
+ *Ryuta Kamizono*
+
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
* Remove deprecated `#set_state` from the transaction object.
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 013e81c959..9824787658 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -89,18 +89,23 @@ end
namespace :db do
namespace :mysql do
+ connection_arguments = lambda do |connection_name|
+ config = ARTest.config["connections"]["mysql2"][connection_name]
+ ["--user=#{config["username"]}", "--password=#{config["password"]}", ("--host=#{config["host"]}" if config["host"])].join(" ")
+ end
+
desc "Build the MySQL test databases"
task :build do
config = ARTest.config["connections"]["mysql2"]
- %x( mysql --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
- %x( mysql --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
+ %x( mysql #{connection_arguments["arunit"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
+ %x( mysql #{connection_arguments["arunit2"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" )
end
desc "Drop the MySQL test databases"
task :drop do
config = ARTest.config["connections"]["mysql2"]
- %x( mysqladmin --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -f drop #{config["arunit"]["database"]} )
- %x( mysqladmin --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -f drop #{config["arunit2"]["database"]} )
+ %x( mysqladmin #{connection_arguments["arunit"]} -f drop #{config["arunit"]["database"]} )
+ %x( mysqladmin #{connection_arguments["arunit2"]} -f drop #{config["arunit2"]["database"]} )
end
desc "Rebuild the MySQL test databases"
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index de94f9693f..7d66158f47 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -74,6 +74,7 @@ module ActiveRecord
autoload :Translation
autoload :Validations
autoload :SecureToken
+ autoload :DatabaseSelector, "active_record/middleware/database_selector"
eager_autoload do
autoload :ActiveRecordError, "active_record/errors"
@@ -153,6 +154,12 @@ module ActiveRecord
end
end
+ module Middleware
+ extend ActiveSupport::Autoload
+
+ autoload :DatabaseSelector, "active_record/middleware/database_selector"
+ end
+
module Tasks
extend ActiveSupport::Autoload
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 5d0927f17d..0bb63b97ae 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -17,6 +17,23 @@ module ActiveRecord
# CollectionAssociation
# HasManyAssociation + ForeignAssociation
# HasManyThroughAssociation + ThroughAssociation
+ #
+ # Associations in Active Record are middlemen between the object that
+ # holds the association, known as the <tt>owner</tt>, and the associated
+ # result set, known as the <tt>target</tt>. Association metadata is available in
+ # <tt>reflection</tt>, which is an instance of <tt>ActiveRecord::Reflection::AssociationReflection</tt>.
+ #
+ # For example, given
+ #
+ # class Blog < ActiveRecord::Base
+ # has_many :posts
+ # end
+ #
+ # blog = Blog.first
+ #
+ # The association of <tt>blog.posts</tt> has the object +blog+ as its
+ # <tt>owner</tt>, the collection of its posts as <tt>target</tt>, and
+ # the <tt>reflection</tt> object represents a <tt>:has_many</tt> macro.
class Association #:nodoc:
attr_reader :owner, :target, :reflection
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 4a25567c9d..b0c0beac0e 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -109,9 +109,8 @@ module ActiveRecord
end
end
- # Add +records+ to this association. Returns +self+ so method calls may
- # be chained. Since << flattens its argument list and inserts each record,
- # +push+ and +concat+ behave identically.
+ # Add +records+ to this association. Since +<<+ flattens its argument list
+ # and inserts each record, +push+ and +concat+ behave identically.
def concat(*records)
records = records.flatten
if owner.new_record?
@@ -210,7 +209,8 @@ module ActiveRecord
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
- if !find_target? || loaded?
+ if !find_target?
+ loaded! unless loaded?
target.size
elsif @association_ids
@association_ids.size
@@ -233,7 +233,7 @@ module ActiveRecord
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded? || @association_ids
+ if loaded? || @association_ids || reflection.has_cached_counter?
size.zero?
else
target.empty? && !scope.exists?
@@ -347,7 +347,6 @@ module ActiveRecord
add_to_target(record) do
result = insert_record(record, true, raise) {
@_was_loaded = loaded?
- @association_ids = nil
}
end
raise ActiveRecord::Rollback unless result
@@ -384,6 +383,7 @@ module ActiveRecord
delete_records(existing_records, method) if existing_records.any?
@target -= records
+ @association_ids = nil
records.each { |record| callback(:after_remove, record) }
end
@@ -424,7 +424,6 @@ module ActiveRecord
unless owner.new_record?
result &&= insert_record(record, true, raise) {
@_was_loaded = loaded?
- @association_ids = nil
}
end
end
@@ -447,6 +446,7 @@ module ActiveRecord
if index
target[index] = record
elsif @_was_loaded || !loaded?
+ @association_ids = nil
target << record
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 4fbbc713e4..edcb44f0fc 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -2,11 +2,8 @@
module ActiveRecord
module Associations
- # Association proxies in Active Record are middlemen between the object that
- # holds the association, known as the <tt>@owner</tt>, and the actual associated
- # object, known as the <tt>@target</tt>. The kind of association any proxy is
- # about is available in <tt>@reflection</tt>. That's an instance of the class
- # ActiveRecord::Reflection::AssociationReflection.
+ # Collection proxies in Active Record are middlemen between an
+ # <tt>association</tt>, and its <tt>target</tt> result set.
#
# For example, given
#
@@ -16,14 +13,14 @@ module ActiveRecord
#
# blog = Blog.first
#
- # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
- # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
- # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
+ # The collection proxy returned by <tt>blog.posts</tt> is built from a
+ # <tt>:has_many</tt> <tt>association</tt>, and delegates to a collection
+ # of posts as the <tt>target</tt>.
#
- # This class delegates unknown methods to <tt>@target</tt> via
- # <tt>method_missing</tt>.
+ # This class delegates unknown methods to the <tt>association</tt>'s
+ # relation class via a delegate cache.
#
- # The <tt>@target</tt> object is not \loaded until needed. For example,
+ # The <tt>target</tt> result set is not loaded until needed. For example,
#
# blog.posts.count
#
@@ -366,34 +363,6 @@ module ActiveRecord
@association.create!(attributes, &block)
end
- # Add one or more records to the collection by setting their foreign keys
- # to the association's primary key. Since #<< flattens its argument list and
- # inserts each record, +push+ and #concat behave identically. Returns +self+
- # so method calls may be chained.
- #
- # class Person < ActiveRecord::Base
- # has_many :pets
- # end
- #
- # person.pets.size # => 0
- # person.pets.concat(Pet.new(name: 'Fancy-Fancy'))
- # person.pets.concat(Pet.new(name: 'Spook'), Pet.new(name: 'Choo-Choo'))
- # person.pets.size # => 3
- #
- # person.id # => 1
- # person.pets
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
- #
- # person.pets.concat([Pet.new(name: 'Brain'), Pet.new(name: 'Benny')])
- # person.pets.size # => 5
- def concat(*records)
- @association.concat(*records)
- end
-
# Replaces this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
#
@@ -1033,8 +1002,9 @@ module ActiveRecord
end
# Adds one or more +records+ to the collection by setting their foreign keys
- # to the association's primary key. Returns +self+, so several appends may be
- # chained together.
+ # to the association's primary key. Since +<<+ flattens its argument list and
+ # inserts each record, +push+ and +concat+ behave identically. Returns +self+
+ # so several appends may be chained together.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -1057,6 +1027,7 @@ module ActiveRecord
end
alias_method :push, :<<
alias_method :append, :<<
+ alias_method :concat, :<<
def prepend(*args)
raise NoMethodError, "prepend on association is not defined. Please use <<, push or append"
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index eb22db838c..6f67934a79 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -36,14 +36,6 @@ module ActiveRecord
super
end
- def empty?
- if reflection.has_cached_counter?
- size.zero?
- else
- super
- end
- end
-
private
# Returns the number of records in this collection.
@@ -60,20 +52,24 @@ module ActiveRecord
# If the collection is empty the target is set to an empty array and
# the loaded flag is set to true as well.
def count_records
- count = if reflection.has_cached_counter?
- owner._read_attribute(reflection.counter_cache_column).to_i
- else
- scope.count(:all)
- end
+ count = counter_cache_value || scope.count(:all)
# If there's nothing in the database and @target has no new records
# we are certain the current target is an empty array. This is a
# documented side-effect of the method that may avoid an extra SELECT.
- (@target ||= []) && loaded! if count == 0
+ loaded! if count == 0
[association_scope.limit_value, count].compact.min
end
+ def counter_cache_value
+ reflection.has_cached_counter? ? owner._read_attribute(reflection.counter_cache_column).to_i : nil
+ end
+
+ def find_target?
+ super && !counter_cache_value&.zero?
+ end
+
def update_counter(difference, reflection = reflection())
if reflection.has_cached_counter?
owner.increment!(reflection.counter_cache_column, difference)
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index d6f7359055..041e62077c 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -42,11 +42,10 @@ module ActiveRecord
def associate_records_to_owner(owner, records)
association = owner.association(reflection.name)
- association.loaded!
if reflection.collection?
- association.target.concat(records)
+ association.target = records
else
- association.target = records.first unless records.empty?
+ association.target = records.first
end
end
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 99934a0e31..1c8df3c08a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -185,7 +185,7 @@ module ActiveRecord
def wait_poll(timeout)
@num_waiting += 1
- t0 = Time.now
+ t0 = Concurrent.monotonic_time
elapsed = 0
loop do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@@ -194,7 +194,7 @@ module ActiveRecord
return remove if any?
- elapsed = Time.now - t0
+ elapsed = Concurrent.monotonic_time - t0
if elapsed >= timeout
msg = "could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use" %
[timeout, elapsed]
@@ -686,13 +686,13 @@ module ActiveRecord
end
newly_checked_out = []
- timeout_time = Time.now + (@checkout_timeout * 2)
+ timeout_time = Concurrent.monotonic_time + (@checkout_timeout * 2)
@available.with_a_bias_for(Thread.current) do
loop do
synchronize do
return if collected_conns.size == @connections.size && @now_connecting == 0
- remaining_timeout = timeout_time - Time.now
+ remaining_timeout = timeout_time - Concurrent.monotonic_time
remaining_timeout = 0 if remaining_timeout < 0
conn = checkout_for_exclusive_access(remaining_timeout)
collected_conns << conn
@@ -1006,7 +1006,16 @@ module ActiveRecord
# for (not necessarily the current class).
def retrieve_connection(spec_name) #:nodoc:
pool = retrieve_connection_pool(spec_name)
- raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool
+
+ unless pool
+ # multiple database application
+ if ActiveRecord::Base.connection_handler != ActiveRecord::Base.default_connection_handler
+ raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found for the '#{ActiveRecord::Base.current_role}' role."
+ else
+ raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found."
+ end
+ end
+
pool.connection
end
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 8aeb934ec2..4e55fcae2f 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -17,7 +17,7 @@ module ActiveRecord
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
def #{method_name}(*)
- clear_query_cache if @query_cache_enabled
+ ActiveRecord::Base.clear_query_caches_for_current_thread if @query_cache_enabled
super
end
end_code
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index 2cb0a2a4df..7d20825a75 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -15,7 +15,7 @@ module ActiveRecord
end
delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
- :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options,
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options,
to: :@conn, private: true
private
@@ -50,7 +50,7 @@ module ActiveRecord
statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) })
end
- if supports_foreign_keys_in_create?
+ if supports_foreign_keys?
statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
end
@@ -127,6 +127,9 @@ module ActiveRecord
end
def foreign_key_in_create(from_table, to_table, options)
+ prefix = ActiveRecord::Base.table_name_prefix
+ suffix = ActiveRecord::Base.table_name_suffix
+ to_table = "#{prefix}#{to_table}#{suffix}"
options = foreign_key_options(from_table, to_table, options)
accept ForeignKeyDefinition.new(from_table, to_table, options)
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 db489143af..a27751fd70 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/deprecation"
-
module ActiveRecord
module ConnectionAdapters #:nodoc:
# Abstract representation of an index definition on a table. Instances of
@@ -104,7 +102,7 @@ module ActiveRecord
alias validated? validate?
def export_name_on_schema_dump?
- name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern
+ !ActiveRecord::SchemaDumper.fk_ignore_pattern.match?(name) if name
end
def defined_for?(to_table_ord = nil, to_table: nil, **options)
@@ -200,41 +198,44 @@ module ActiveRecord
end
module ColumnMethods
+ extend ActiveSupport::Concern
+
# Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea.
def primary_key(name, type = :primary_key, **options)
column(name, type, options.merge(primary_key: true))
end
+ ##
+ # :method: column
+ # :call-seq: column(name, type, options = {})
+ #
# Appends a column or columns of a specified type.
#
# t.string(:goat)
# t.string(:goat, :sheep)
#
# See TableDefinition#column
- [
- :bigint,
- :binary,
- :boolean,
- :date,
- :datetime,
- :decimal,
- :float,
- :integer,
- :json,
- :string,
- :text,
- :time,
- :timestamp,
- :virtual,
- ].each do |column_type|
- module_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{column_type}(*args, **options)
- args.each { |name| column(name, :#{column_type}, options) }
+
+ included do
+ define_column_methods :bigint, :binary, :boolean, :date, :datetime, :decimal,
+ :float, :integer, :json, :string, :text, :time, :timestamp, :virtual
+
+ alias :numeric :decimal
+ end
+
+ class_methods do
+ private def define_column_methods(*column_types) # :nodoc:
+ column_types.each do |column_type|
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{column_type}(*names, **options)
+ raise ArgumentError, "Missing column name(s) for #{column_type}" if names.empty?
+ names.each { |name| column(name, :#{column_type}, options) }
+ end
+ RUBY
end
- CODE
+ end
end
- alias_method :numeric, :decimal
end
# Represents the schema of an SQL table in an abstract way. This class
@@ -259,10 +260,9 @@ module ActiveRecord
include ColumnMethods
attr_reader :name, :temporary, :if_not_exists, :options, :as, :comment, :indexes, :foreign_keys
- attr_writer :indexes
- deprecate :indexes=
def initialize(
+ conn,
name,
temporary: false,
if_not_exists: false,
@@ -271,6 +271,7 @@ module ActiveRecord
comment: nil,
**
)
+ @conn = conn
@columns_hash = {}
@indexes = []
@foreign_keys = []
@@ -397,10 +398,7 @@ module ActiveRecord
end
def foreign_key(table_name, options = {}) # :nodoc:
- table_name_prefix = ActiveRecord::Base.table_name_prefix
- table_name_suffix = ActiveRecord::Base.table_name_suffix
- table_name = "#{table_name_prefix}#{table_name}#{table_name_suffix}"
- foreign_keys.push([table_name, options])
+ foreign_keys << [table_name, options]
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
@@ -410,6 +408,10 @@ module ActiveRecord
def timestamps(**options)
options[:null] = false if options[:null].nil?
+ if !options.key?(:precision) && @conn.supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
@@ -517,6 +519,7 @@ module ActiveRecord
# t.json
# t.virtual
# t.remove
+ # t.remove_foreign_key
# t.remove_references
# t.remove_belongs_to
# t.remove_index
@@ -680,15 +683,26 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_references
- # Adds a foreign key.
+ # Adds a foreign key to the table using a supplied table name.
#
# t.foreign_key(:authors)
+ # t.foreign_key(:authors, column: :author_id, primary_key: "id")
#
# See {connection.add_foreign_key}[rdoc-ref:SchemaStatements#add_foreign_key]
def foreign_key(*args)
@base.add_foreign_key(name, *args)
end
+ # Removes the given foreign key from the table.
+ #
+ # t.remove_foreign_key(:authors)
+ # t.remove_foreign_key(column: :author_id)
+ #
+ # See {connection.remove_foreign_key}[rdoc-ref:SchemaStatements#remove_foreign_key]
+ def remove_foreign_key(*args)
+ @base.remove_foreign_key(name, *args)
+ end
+
# Checks to see if a foreign key exists.
#
# t.foreign_key(:authors) unless t.foreign_key_exists?(:authors)
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 208c8c9c64..78e153bcc9 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -130,11 +130,11 @@ module ActiveRecord
# column_exists?(:suppliers, :name, :string, null: false)
# column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
#
- def column_exists?(table_name, column_name, type = nil, options = {})
+ def column_exists?(table_name, column_name, type = nil, **options)
column_name = column_name.to_s
checks = []
checks << lambda { |c| c.name == column_name }
- checks << lambda { |c| c.type == type } if type
+ checks << lambda { |c| c.type == type.to_sym rescue nil } if type
column_options_keys.each do |attr|
checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr)
end
@@ -1029,9 +1029,7 @@ module ActiveRecord
end
def foreign_key_column_for(table_name) # :nodoc:
- prefix = Base.table_name_prefix
- suffix = Base.table_name_suffix
- name = table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s
+ name = strip_table_name_prefix_and_suffix(table_name)
"#{name.singularize}_id"
end
@@ -1129,6 +1127,10 @@ module ActiveRecord
def add_timestamps(table_name, options = {})
options[:null] = false if options[:null].nil?
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
add_column table_name, :created_at, :datetime, options
add_column table_name, :updated_at, :datetime, options
end
@@ -1290,7 +1292,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- TableDefinition.new(*args)
+ TableDefinition.new(self, *args)
end
def create_alter_table(name)
@@ -1324,6 +1326,12 @@ module ActiveRecord
{ column: column_names }
end
+ def strip_table_name_prefix_and_suffix(table_name)
+ prefix = Base.table_name_prefix
+ suffix = Base.table_name_suffix
+ table_name.to_s =~ /#{prefix}(.+)#{suffix}/ ? $1 : table_name.to_s
+ end
+
def foreign_key_name(table_name, options)
options.fetch(:name) do
identifier = "#{table_name}_#{options.fetch(:column)}_fk"
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 112f376d0a..c9e84e48cc 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -205,9 +205,12 @@ module ActiveRecord
run_commit_callbacks: run_commit_callbacks)
end
- transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled?
+ if @connection.supports_lazy_transactions? && lazy_transactions_enabled? && options[:_lazy] != false
+ @has_unmaterialized_transactions = true
+ else
+ transaction.materialize!
+ end
@stack.push(transaction)
- @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions?
transaction
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index d1ff32df3f..3c9510e469 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -77,9 +77,6 @@ module ActiveRecord
SIMPLE_INT = /\A\d+\z/
- attr_writer :visitor
- deprecate :visitor=
-
attr_accessor :pool
attr_reader :schema_cache, :visitor, :owner, :logger, :lock, :prepared_statements, :prevent_writes
alias :in_use? :owner
@@ -338,6 +335,7 @@ module ActiveRecord
def supports_foreign_keys_in_create?
supports_foreign_keys?
end
+ deprecate :supports_foreign_keys_in_create?
# Does this adapter support views?
def supports_views?
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 5479ddab71..569c146f92 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -29,7 +29,7 @@ module ActiveRecord
NATIVE_DATABASE_TYPES = {
primary_key: "bigint auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
- text: { name: "text", limit: 65535 },
+ text: { name: "text" },
integer: { name: "int", limit: 4 },
float: { name: "float", limit: 24 },
decimal: { name: "decimal" },
@@ -37,7 +37,8 @@ module ActiveRecord
timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
- binary: { name: "blob", limit: 65535 },
+ binary: { name: "blob" },
+ blob: { name: "blob" },
boolean: { name: "tinyint", limit: 1 },
json: { name: "json" },
}
@@ -165,9 +166,9 @@ module ActiveRecord
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds)}"
- start = Time.now
+ start = Concurrent.monotonic_time
result = exec_query(sql, "EXPLAIN", binds)
- elapsed = Time.now - start
+ elapsed = Concurrent.monotonic_time - start
MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
end
@@ -709,6 +710,12 @@ module ActiveRecord
end
def add_timestamps_for_alter(table_name, options = {})
+ options[:null] = false if options[:null].nil?
+
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
[add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index 2ed4ad16ae..8cb6b64fec 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -4,48 +4,56 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
module ColumnMethods
- def blob(*args, **options)
- args.each { |name| column(name, :blob, options) }
- end
+ extend ActiveSupport::Concern
- def tinyblob(*args, **options)
- args.each { |name| column(name, :tinyblob, options) }
- end
+ ##
+ # :method: blob
+ # :call-seq: blob(*names, **options)
- def mediumblob(*args, **options)
- args.each { |name| column(name, :mediumblob, options) }
- end
+ ##
+ # :method: tinyblob
+ # :call-seq: tinyblob(*names, **options)
- def longblob(*args, **options)
- args.each { |name| column(name, :longblob, options) }
- end
+ ##
+ # :method: mediumblob
+ # :call-seq: mediumblob(*names, **options)
- def tinytext(*args, **options)
- args.each { |name| column(name, :tinytext, options) }
- end
+ ##
+ # :method: longblob
+ # :call-seq: longblob(*names, **options)
- def mediumtext(*args, **options)
- args.each { |name| column(name, :mediumtext, options) }
- end
+ ##
+ # :method: tinytext
+ # :call-seq: tinytext(*names, **options)
- def longtext(*args, **options)
- args.each { |name| column(name, :longtext, options) }
- end
+ ##
+ # :method: mediumtext
+ # :call-seq: mediumtext(*names, **options)
- def unsigned_integer(*args, **options)
- args.each { |name| column(name, :unsigned_integer, options) }
- end
+ ##
+ # :method: longtext
+ # :call-seq: longtext(*names, **options)
- def unsigned_bigint(*args, **options)
- args.each { |name| column(name, :unsigned_bigint, options) }
- end
+ ##
+ # :method: unsigned_integer
+ # :call-seq: unsigned_integer(*names, **options)
- def unsigned_float(*args, **options)
- args.each { |name| column(name, :unsigned_float, options) }
- end
+ ##
+ # :method: unsigned_bigint
+ # :call-seq: unsigned_bigint(*names, **options)
+
+ ##
+ # :method: unsigned_float
+ # :call-seq: unsigned_float(*names, **options)
+
+ ##
+ # :method: unsigned_decimal
+ # :call-seq: unsigned_decimal(*names, **options)
- def unsigned_decimal(*args, **options)
- args.each { |name| column(name, :unsigned_decimal, options) }
+ included do
+ define_column_methods :blob, :tinyblob, :mediumblob, :longblob,
+ :tinytext, :mediumtext, :longtext, :unsigned_integer, :unsigned_bigint,
+ :unsigned_float, :unsigned_decimal
end
end
@@ -56,6 +64,16 @@ module ActiveRecord
case type
when :virtual
type = options[:type]
+ when :text, :blob, :binary
+ case (size = options[:size])&.to_s
+ when "tiny", "medium", "long"
+ sql_type = @conn.native_database_types[type][:name]
+ type = "#{size}#{sql_type}"
+ else
+ raise ArgumentError, <<~MSG unless size.nil?
+ #{size.inspect} is invalid :size value. Only :tiny, :medium, and :long are allowed.
+ MSG
+ end
when :primary_key
type = :integer
options[:limit] ||= 8
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 d23178e43c..57518b02fa 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -10,6 +10,10 @@ module ActiveRecord
spec[:unsigned] = "true" if column.unsigned?
spec[:auto_increment] = "true" if column.auto_increment?
+ if /\A(?<size>tiny|medium|long)(?:text|blob)/ =~ column.sql_type
+ spec = { size: size.to_sym.inspect }.merge!(spec)
+ end
+
if @connection.supports_virtual_columns? && column.virtual?
spec[:as] = extract_expression_for_virtual_column(column)
spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra)
@@ -37,13 +41,15 @@ module ActiveRecord
case column.sql_type
when /\Atimestamp\b/
:timestamp
- when "tinyblob"
- :blob
else
super
end
end
+ def schema_limit(column)
+ super unless /\A(?:tiny|medium|long)?(?:text|blob)/.match?(column.sql_type)
+ end
+
def schema_precision(column)
super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index 47b5c4b9ec..e9484a08de 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -127,7 +127,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- MySQL::TableDefinition.new(*args)
+ MySQL::TableDefinition.new(self, *args)
end
def new_column_from_field(table_name, field)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index dc4a0bb26e..3bb7c52899 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -4,6 +4,8 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module ColumnMethods
+ extend ActiveSupport::Concern
+
# Defines the primary key field.
# Use of the native PostgreSQL UUID type is supported, and can be used
# by defining your tables as such:
@@ -51,124 +53,131 @@ module ActiveRecord
super
end
- def bigserial(*args, **options)
- args.each { |name| column(name, :bigserial, options) }
- end
+ ##
+ # :method: bigserial
+ # :call-seq: bigserial(*names, **options)
- def bit(*args, **options)
- args.each { |name| column(name, :bit, options) }
- end
+ ##
+ # :method: bit
+ # :call-seq: bit(*names, **options)
- def bit_varying(*args, **options)
- args.each { |name| column(name, :bit_varying, options) }
- end
+ ##
+ # :method: bit_varying
+ # :call-seq: bit_varying(*names, **options)
- def cidr(*args, **options)
- args.each { |name| column(name, :cidr, options) }
- end
+ ##
+ # :method: cidr
+ # :call-seq: cidr(*names, **options)
- def citext(*args, **options)
- args.each { |name| column(name, :citext, options) }
- end
+ ##
+ # :method: citext
+ # :call-seq: citext(*names, **options)
- def daterange(*args, **options)
- args.each { |name| column(name, :daterange, options) }
- end
+ ##
+ # :method: daterange
+ # :call-seq: daterange(*names, **options)
- def hstore(*args, **options)
- args.each { |name| column(name, :hstore, options) }
- end
+ ##
+ # :method: hstore
+ # :call-seq: hstore(*names, **options)
- def inet(*args, **options)
- args.each { |name| column(name, :inet, options) }
- end
+ ##
+ # :method: inet
+ # :call-seq: inet(*names, **options)
- def interval(*args, **options)
- args.each { |name| column(name, :interval, options) }
- end
+ ##
+ # :method: interval
+ # :call-seq: interval(*names, **options)
- def int4range(*args, **options)
- args.each { |name| column(name, :int4range, options) }
- end
+ ##
+ # :method: int4range
+ # :call-seq: int4range(*names, **options)
- def int8range(*args, **options)
- args.each { |name| column(name, :int8range, options) }
- end
+ ##
+ # :method: int8range
+ # :call-seq: int8range(*names, **options)
- def jsonb(*args, **options)
- args.each { |name| column(name, :jsonb, options) }
- end
+ ##
+ # :method: jsonb
+ # :call-seq: jsonb(*names, **options)
- def ltree(*args, **options)
- args.each { |name| column(name, :ltree, options) }
- end
+ ##
+ # :method: ltree
+ # :call-seq: ltree(*names, **options)
- def macaddr(*args, **options)
- args.each { |name| column(name, :macaddr, options) }
- end
+ ##
+ # :method: macaddr
+ # :call-seq: macaddr(*names, **options)
- def money(*args, **options)
- args.each { |name| column(name, :money, options) }
- end
+ ##
+ # :method: money
+ # :call-seq: money(*names, **options)
- def numrange(*args, **options)
- args.each { |name| column(name, :numrange, options) }
- end
+ ##
+ # :method: numrange
+ # :call-seq: numrange(*names, **options)
- def oid(*args, **options)
- args.each { |name| column(name, :oid, options) }
- end
+ ##
+ # :method: oid
+ # :call-seq: oid(*names, **options)
- def point(*args, **options)
- args.each { |name| column(name, :point, options) }
- end
+ ##
+ # :method: point
+ # :call-seq: point(*names, **options)
- def line(*args, **options)
- args.each { |name| column(name, :line, options) }
- end
+ ##
+ # :method: line
+ # :call-seq: line(*names, **options)
- def lseg(*args, **options)
- args.each { |name| column(name, :lseg, options) }
- end
+ ##
+ # :method: lseg
+ # :call-seq: lseg(*names, **options)
- def box(*args, **options)
- args.each { |name| column(name, :box, options) }
- end
+ ##
+ # :method: box
+ # :call-seq: box(*names, **options)
- def path(*args, **options)
- args.each { |name| column(name, :path, options) }
- end
+ ##
+ # :method: path
+ # :call-seq: path(*names, **options)
- def polygon(*args, **options)
- args.each { |name| column(name, :polygon, options) }
- end
+ ##
+ # :method: polygon
+ # :call-seq: polygon(*names, **options)
- def circle(*args, **options)
- args.each { |name| column(name, :circle, options) }
- end
+ ##
+ # :method: circle
+ # :call-seq: circle(*names, **options)
- def serial(*args, **options)
- args.each { |name| column(name, :serial, options) }
- end
+ ##
+ # :method: serial
+ # :call-seq: serial(*names, **options)
- def tsrange(*args, **options)
- args.each { |name| column(name, :tsrange, options) }
- end
+ ##
+ # :method: tsrange
+ # :call-seq: tsrange(*names, **options)
- def tstzrange(*args, **options)
- args.each { |name| column(name, :tstzrange, options) }
- end
+ ##
+ # :method: tstzrange
+ # :call-seq: tstzrange(*names, **options)
- def tsvector(*args, **options)
- args.each { |name| column(name, :tsvector, options) }
- end
+ ##
+ # :method: tsvector
+ # :call-seq: tsvector(*names, **options)
- def uuid(*args, **options)
- args.each { |name| column(name, :uuid, options) }
- end
+ ##
+ # :method: uuid
+ # :call-seq: uuid(*names, **options)
+
+ ##
+ # :method: xml
+ # :call-seq: xml(*names, **options)
- def xml(*args, **options)
- args.each { |name| column(name, :xml, options) }
+ included do
+ define_column_methods :bigserial, :bit, :bit_varying, :cidr, :citext, :daterange,
+ :hstore, :inet, :interval, :int4range, :int8range, :jsonb, :ltree, :macaddr,
+ :money, :numrange, :oid, :point, :line, :lseg, :box, :path, :polygon, :circle,
+ :serial, :tsrange, :tstzrange, :tsvector, :uuid, :xml
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 7cf371be68..946436f7f9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -637,7 +637,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- PostgreSQL::TableDefinition.new(*args)
+ PostgreSQL::TableDefinition.new(self, *args)
end
def create_alter_table(name)
@@ -716,6 +716,12 @@ module ActiveRecord
end
def add_timestamps_for_alter(table_name, options = {})
+ options[:null] = false if options[:null].nil?
+
+ if !options.key?(:precision) && supports_datetime_with_precision?
+ options[:precision] = 6
+ end
+
[add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 381d5ab29b..95beeb4cae 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -236,6 +236,8 @@ module ActiveRecord
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@local_tz = nil
+ @default_timezone = nil
+ @timestamp_decoder = nil
@max_identifier_length = nil
configure_connection
@@ -628,6 +630,10 @@ module ActiveRecord
def exec_no_cache(sql, name, binds)
materialize_transactions
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ update_typemap_for_default_timezone
+
type_casted_binds = type_casted_binds(binds)
log(sql, name, binds, type_casted_binds) do
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
@@ -638,6 +644,7 @@ module ActiveRecord
def exec_cache(sql, name, binds)
materialize_transactions
+ update_typemap_for_default_timezone
stmt_key = prepare_statement(sql, binds)
type_casted_binds = type_casted_binds(binds)
@@ -826,6 +833,18 @@ module ActiveRecord
@connection.type_map_for_queries = map
end
+ def update_typemap_for_default_timezone
+ if @default_timezone != ActiveRecord::Base.default_timezone && @timestamp_decoder
+ decoder_class = ActiveRecord::Base.default_timezone == :utc ?
+ PG::TextDecoder::TimestampUtc :
+ PG::TextDecoder::TimestampWithoutTimeZone
+
+ @timestamp_decoder = decoder_class.new(@timestamp_decoder.to_h)
+ @connection.type_map_for_results.add_coder(@timestamp_decoder)
+ @default_timezone = ActiveRecord::Base.default_timezone
+ end
+ end
+
def add_pg_decoders
coders_by_name = {
"int2" => PG::TextDecoder::Integer,
@@ -836,6 +855,13 @@ module ActiveRecord
"float8" => PG::TextDecoder::Float,
"bool" => PG::TextDecoder::Boolean,
}
+
+ if defined?(PG::TextDecoder::TimestampUtc)
+ # Use native PG encoders available since pg-1.1
+ coders_by_name["timestamp"] = PG::TextDecoder::TimestampUtc
+ coders_by_name["timestamptz"] = PG::TextDecoder::TimestampWithTimeZone
+ end
+
known_coder_types = coders_by_name.keys.map { |n| quote(n) }
query = <<~SQL % known_coder_types.join(", ")
SELECT t.oid, t.typname
@@ -851,6 +877,10 @@ module ActiveRecord
map = PG::TypeMapByOid.new
coders.each { |coder| map.add_coder(coder) }
@connection.type_map_for_results = map
+
+ # extract timestamp decoder for use in update_typemap_for_default_timezone
+ @timestamp_decoder = coders.find { |coder| coder.name == "timestamp" }
+ update_typemap_for_default_timezone
end
def construct_coder(row, coder_class)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index 8650c07bab..0a637d81fa 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -52,6 +52,34 @@ module ActiveRecord
end.compact
end
+ def add_foreign_key(from_table, to_table, **options)
+ alter_table(from_table) do |definition|
+ to_table = strip_table_name_prefix_and_suffix(to_table)
+ definition.foreign_key(to_table, options)
+ end
+ end
+
+ def remove_foreign_key(from_table, to_table = nil, **options)
+ to_table ||= options[:to_table]
+ options = options.except(:name, :to_table)
+ foreign_keys = foreign_keys(from_table)
+
+ fkey = foreign_keys.detect do |fk|
+ table = to_table || begin
+ table = options[:column].to_s.delete_suffix("_id")
+ Base.pluralize_table_names ? table.pluralize : table
+ end
+ table = strip_table_name_prefix_and_suffix(table)
+ fk_to_table = strip_table_name_prefix_and_suffix(fk.to_table)
+ fk_to_table == table && options.all? { |k, v| fk.options[k].to_s == v.to_s }
+ end || raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{to_table}")
+
+ alter_table(from_table, foreign_keys) do |definition|
+ fk_to_table = strip_table_name_prefix_and_suffix(fkey.to_table)
+ definition.foreign_keys.delete([fk_to_table, fkey.options])
+ end
+ end
+
def create_schema_dumper(options)
SQLite3::SchemaDumper.create(self, options)
end
@@ -62,7 +90,7 @@ module ActiveRecord
end
def create_table_definition(*args)
- SQLite3::TableDefinition.new(*args)
+ SQLite3::TableDefinition.new(self, *args)
end
def new_column_from_field(table_name, field)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 14dbd20bcd..9f28fdf749 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -9,7 +9,7 @@ require "active_record/connection_adapters/sqlite3/schema_definitions"
require "active_record/connection_adapters/sqlite3/schema_dumper"
require "active_record/connection_adapters/sqlite3/schema_statements"
-gem "sqlite3", "~> 1.3.6"
+gem "sqlite3", "~> 1.3", ">= 1.3.6"
require "sqlite3"
module ActiveRecord
@@ -121,7 +121,7 @@ module ActiveRecord
true
end
- def supports_foreign_keys_in_create?
+ def supports_foreign_keys?
true
end
@@ -320,6 +320,9 @@ module ActiveRecord
def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
alter_table(table_name) do |definition|
definition.remove_column column_name
+ definition.foreign_keys.delete_if do |_, fk_options|
+ fk_options[:column] == column_name.to_s
+ end
end
end
@@ -421,9 +424,8 @@ module ActiveRecord
type.to_sym == :primary_key || options[:primary_key]
end
- def alter_table(table_name, options = {})
+ def alter_table(table_name, foreign_keys = foreign_keys(table_name), **options)
altered_table_name = "a#{table_name}"
- foreign_keys = foreign_keys(table_name)
caller = lambda do |definition|
rename = options[:rename] || {}
@@ -431,7 +433,8 @@ module ActiveRecord
if column = rename[fk.options[:column]]
fk.options[:column] = column
end
- definition.foreign_key(fk.to_table, fk.options)
+ to_table = strip_table_name_prefix_and_suffix(fk.to_table)
+ definition.foreign_key(to_table, fk.options)
end
yield definition if block_given?
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 4a941055d1..53069cd899 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -158,10 +158,6 @@ module ActiveRecord
end
def with_handler(handler_key, &blk) # :nodoc:
- unless ActiveRecord::Base.connection_handlers.keys.include?(handler_key)
- raise ArgumentError, "The #{handler_key} role does not exist. Add it by establishing a connection with `connects_to` or use an existing role (#{ActiveRecord::Base.connection_handlers.keys.join(", ")})."
- end
-
handler = lookup_connection_handler(handler_key)
swap_connection_handler(handler, &blk)
end
@@ -180,6 +176,15 @@ module ActiveRecord
config_hash
end
+ # Clears the query cache for all connections associated with the current thread.
+ def clear_query_caches_for_current_thread
+ ActiveRecord::Base.connection_handlers.each_value do |handler|
+ handler.connection_pool_list.each do |pool|
+ pool.connection.clear_query_cache if pool.active_connection?
+ end
+ end
+ end
+
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 369d63e40a..c67980173f 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -101,6 +101,7 @@ module ActiveRecord
# environment where dumping schema is rarely needed.
mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true
+ mattr_accessor :database_selector, instance_writer: false
##
# :singleton-method:
# Specifies which database schemas to dump when calling db:structure:dump.
@@ -124,6 +125,10 @@ module ActiveRecord
mattr_accessor :connection_handlers, instance_accessor: false, default: {}
+ mattr_accessor :writing_role, instance_accessor: false, default: :writing
+
+ mattr_accessor :reading_role, instance_accessor: false, default: :reading
+
class_attribute :default_connection_handler, instance_writer: false
self.filter_attributes = []
@@ -137,7 +142,6 @@ module ActiveRecord
end
self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
- self.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
end
module ClassMethods
@@ -473,6 +477,14 @@ module ActiveRecord
end
end
+ def present? # :nodoc:
+ true
+ end
+
+ def blank? # :nodoc:
+ false
+ end
+
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
# attributes will be marked as read only since they cannot be saved.
def readonly?
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
index 11aed6c002..73adf66684 100644
--- a/activerecord/lib/active_record/database_configurations.rb
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -134,9 +134,11 @@ module ActiveRecord
end
def build_db_config_from_hash(env_name, spec_name, config)
- if url = config["url"]
+ if config.has_key?("url")
+ url = config["url"]
config_without_url = config.dup
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)
diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb
index 81917fc4c1..8e8aa69478 100644
--- a/activerecord/lib/active_record/database_configurations/url_config.rb
+++ b/activerecord/lib/active_record/database_configurations/url_config.rb
@@ -56,12 +56,17 @@ module ActiveRecord
end
private
- def build_config(original_config, url)
- if /^jdbc:/.match?(url)
- hash = { "url" => url }
+
+ def build_url_hash(url)
+ if url.nil? || /^jdbc:/.match?(url)
+ { "url" => url }
else
- hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
+ ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash
end
+ end
+
+ def build_config(original_config, url)
+ hash = build_url_hash(url)
if original_config[env_name]
original_config[env_name].merge(hash)
diff --git a/activerecord/lib/active_record/middleware/database_selector.rb b/activerecord/lib/active_record/middleware/database_selector.rb
new file mode 100644
index 0000000000..b5b5df074c
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "active_record/middleware/database_selector/resolver"
+
+module ActiveRecord
+ module Middleware
+ # The DatabaseSelector Middleware provides a framework for automatically
+ # swapping from the primary to the replica database connection. Rails
+ # provides a basic framework to determine when to swap and allows for
+ # applications to write custom strategy classes to override the default
+ # behavior.
+ #
+ # The resolver class defines when the application should switch (i.e. read
+ # from the primary if a write occurred less than 2 seconds ago) and a
+ # resolver context class that sets a value that helps the resolver class
+ # decide when to switch.
+ #
+ # Rails default middleware uses the request's session to set a timestamp
+ # that informs the application when to read from a primary or read from a
+ # replica.
+ #
+ # To use the DatabaseSelector in your application with default settings add
+ # the following options to your environment config:
+ #
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
+ #
+ # New applications will include these lines commented out in the production.rb.
+ #
+ # The default behavior can be changed by setting the config options to a
+ # custom class:
+ #
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # config.active_record.database_resolver = MyResolver
+ # config.active_record.database_resolver_context = MyResolver::MySession
+ class DatabaseSelector
+ def initialize(app, resolver_klass = Resolver, context_klass = Resolver::Session, options = {})
+ @app = app
+ @resolver_klass = resolver_klass
+ @context_klass = context_klass
+ @options = options
+ end
+
+ attr_reader :resolver_klass, :context_klass, :options
+
+ # Middleware that determines which database connection to use in a multiple
+ # database application.
+ def call(env)
+ request = ActionDispatch::Request.new(env)
+
+ select_database(request) do
+ @app.call(env)
+ end
+ end
+
+ private
+
+ def select_database(request, &blk)
+ context = context_klass.call(request)
+ resolver = resolver_klass.call(context, options)
+
+ if reading_request?(request)
+ resolver.read(&blk)
+ else
+ resolver.write(&blk)
+ end
+ end
+
+ def reading_request?(request)
+ request.get? || request.head?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver.rb b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
new file mode 100644
index 0000000000..80b8cd7cae
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require "active_record/middleware/database_selector/resolver/session"
+
+module ActiveRecord
+ module Middleware
+ class DatabaseSelector
+ # The Resolver class is used by the DatabaseSelector middleware to
+ # determine which database the request should use.
+ #
+ # To change the behavior of the Resolver class in your application,
+ # create a custom resolver class that inherits from
+ # DatabaseSelector::Resolver and implements the methods that need to
+ # be changed.
+ #
+ # By default the Resolver class will send read traffic to the replica
+ # if it's been 2 seconds since the last write.
+ class Resolver # :nodoc:
+ SEND_TO_REPLICA_DELAY = 2.seconds
+
+ def self.call(context, options = {})
+ new(context, options)
+ end
+
+ def initialize(context, options = {})
+ @context = context
+ @options = options
+ @delay = @options && @options[:delay] ? @options[:delay] : SEND_TO_REPLICA_DELAY
+ @instrumenter = ActiveSupport::Notifications.instrumenter
+ end
+
+ attr_reader :context, :delay, :instrumenter
+
+ def read(&blk)
+ if read_from_primary?
+ read_from_primary(&blk)
+ else
+ read_from_replica(&blk)
+ end
+ end
+
+ def write(&blk)
+ write_to_primary(&blk)
+ end
+
+ private
+
+ def read_from_primary(&blk)
+ ActiveRecord::Base.connection.while_preventing_writes do
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.writing_role) do
+ instrumenter.instrument("database_selector.active_record.read_from_primary") do
+ yield
+ end
+ end
+ end
+ end
+
+ def read_from_replica(&blk)
+ ActiveRecord::Base.connected_to(role: ActiveRecord::Base.reading_role) do
+ instrumenter.instrument("database_selector.active_record.read_from_replica") do
+ yield
+ end
+ end
+ end
+
+ 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
+ end
+ end
+ end
+
+ def read_from_primary?
+ !time_since_last_write_ok?
+ end
+
+ def send_to_replica_delay
+ delay
+ end
+
+ def time_since_last_write_ok?
+ Time.now - context.last_write_timestamp >= send_to_replica_delay
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
new file mode 100644
index 0000000000..df7af054b7
--- /dev/null
+++ b/activerecord/lib/active_record/middleware/database_selector/resolver/session.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Middleware
+ class DatabaseSelector
+ class Resolver
+ # The session class is used by the DatabaseSelector::Resolver to save
+ # timestamps of the last write in the session.
+ #
+ # The last_write is used to determine whether it's safe to read
+ # from the replica or the request needs to be sent to the primary.
+ class Session # :nodoc:
+ def self.call(request)
+ new(request.session)
+ end
+
+ # Converts time to a timestamp that represents milliseconds since
+ # epoch.
+ def self.convert_time_to_timestamp(time)
+ time.to_i * 1000 + time.usec / 1000
+ end
+
+ # Converts milliseconds since epoch timestamp into a time object.
+ def self.convert_timestamp_to_time(timestamp)
+ timestamp ? Time.at(timestamp / 1000, (timestamp % 1000) * 1000) : Time.at(0)
+ end
+
+ def initialize(session)
+ @session = session
+ end
+
+ attr_reader :session
+
+ def last_write_timestamp
+ self.class.convert_timestamp_to_time(session[:last_write])
+ end
+
+ def update_last_write_timestamp
+ session[:last_write] = self.class.convert_time_to_timestamp(Time.now)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 608182e363..abc939826b 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -16,13 +16,55 @@ module ActiveRecord
V6_0 = Current
class V5_2 < V6_0
+ module TableDefinition
+ def timestamps(**options)
+ options[:precision] ||= nil
+ super
+ end
+ end
+
module CommandRecorder
def invert_transaction(args, &block)
[:transaction, args, block]
end
end
+ def create_table(table_name, **options)
+ if block_given?
+ super { |t| yield compatible_table_definition(t) }
+ else
+ super
+ end
+ end
+
+ def change_table(table_name, **options)
+ if block_given?
+ super { |t| yield compatible_table_definition(t) }
+ else
+ super
+ end
+ end
+
+ def create_join_table(table_1, table_2, **options)
+ if block_given?
+ super { |t| yield compatible_table_definition(t) }
+ else
+ super
+ end
+ end
+
+ def add_timestamps(table_name, **options)
+ options[:precision] ||= nil
+ super
+ end
+
private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ t
+ end
def command_recorder
recorder = super
@@ -35,18 +77,18 @@ module ActiveRecord
class V5_1 < V5_2
def change_column(table_name, column_name, type, options = {})
- if adapter_name == "PostgreSQL"
+ if connection.adapter_name == "PostgreSQL"
super(table_name, column_name, type, options.except(:default, :null, :comment))
- change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
- change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
- change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
+ connection.change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
+ connection.change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ connection.change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
else
super
end
end
def create_table(table_name, options = {})
- if adapter_name == "Mysql2"
+ if connection.adapter_name == "Mysql2"
super(table_name, options: "ENGINE=InnoDB", **options)
else
super
@@ -68,13 +110,13 @@ module ActiveRecord
end
def create_table(table_name, options = {})
- if adapter_name == "PostgreSQL"
+ if connection.adapter_name == "PostgreSQL"
if options[:id] == :uuid && !options.key?(:default)
options[:default] = "uuid_generate_v4()"
end
end
- unless adapter_name == "Mysql2" && options[:id] == :bigint
+ unless connection.adapter_name == "Mysql2" && options[:id] == :bigint
if [:integer, :bigint].include?(options[:id]) && !options.key?(:default)
options[:default] = nil
end
@@ -87,35 +129,12 @@ module ActiveRecord
options[:id] = :integer
end
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
- end
-
- def change_table(table_name, options = {})
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
+ super
end
def create_join_table(table_1, table_2, column_options: {}, **options)
column_options.reverse_merge!(type: :integer)
-
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
+ super
end
def add_column(table_name, column_name, type, options = {})
@@ -136,7 +155,7 @@ module ActiveRecord
class << t
prepend TableDefinition
end
- t
+ super
end
end
@@ -154,33 +173,13 @@ module ActiveRecord
end
end
- def create_table(table_name, options = {})
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
- end
-
- def change_table(table_name, options = {})
- if block_given?
- super do |t|
- yield compatible_table_definition(t)
- end
- else
- super
- end
- end
-
- def add_reference(*, **options)
+ def add_reference(table_name, ref_name, **options)
options[:index] ||= false
super
end
alias :add_belongs_to :add_reference
- def add_timestamps(_, **options)
+ def add_timestamps(table_name, **options)
options[:null] = true if options[:null].nil?
super
end
@@ -191,7 +190,7 @@ module ActiveRecord
if options[:name].present?
options[:name].to_s
else
- index_name(table_name, column: column_names)
+ connection.index_name(table_name, column: column_names)
end
super
end
@@ -211,15 +210,17 @@ module ActiveRecord
end
def index_name_for_remove(table_name, options = {})
- index_name = index_name(table_name, options)
+ index_name = connection.index_name(table_name, options)
- unless index_name_exists?(table_name, index_name)
+ unless connection.index_name_exists?(table_name, index_name)
if options.is_a?(Hash) && options.has_key?(:name)
options_without_column = options.dup
options_without_column.delete :column
- index_name_without_column = index_name(table_name, options_without_column)
+ index_name_without_column = connection.index_name(table_name, options_without_column)
- return index_name_without_column if index_name_exists?(table_name, index_name_without_column)
+ if connection.index_name_exists?(table_name, index_name_without_column)
+ return index_name_without_column
+ end
end
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index c2b60610ce..510a275b4e 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -142,7 +142,7 @@ module ActiveRecord
end
end
- # Deletes the row with a primary key matching the +id+ argument, using a
+ # Deletes the row with a primary key matching the +id+ argument, using an
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
# Record objects are not instantiated, so the object's callbacks are not
# executed, including any <tt>:dependent</tt> association options.
@@ -436,7 +436,7 @@ module ActiveRecord
end
alias update_attributes update
- deprecate :update_attributes
+ deprecate update_attributes: "please, use update instead"
# Updates its receiver just like #update but calls #save! instead
# of +save+, so an exception is raised if the record is invalid and saving will fail.
@@ -450,7 +450,7 @@ module ActiveRecord
end
alias update_attributes! update!
- deprecate :update_attributes!
+ deprecate update_attributes!: "please, use update! instead"
# Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 8c1b2e2be1..2a27f53f06 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -40,8 +40,7 @@ module ActiveRecord
def find_by_sql(sql, binds = [], preparable: nil, &block)
result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable)
column_types = result_set.column_types.dup
- cached_columns_hash = connection.schema_cache.columns_hash(table_name)
- cached_columns_hash.each_key { |k| column_types.delete k }
+ attribute_types.each_key { |k| column_types.delete k }
message_bus = ActiveSupport::Notifications.instrumenter
payload = {
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index 6346a95d57..a1d7c893bf 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -88,6 +88,14 @@ module ActiveRecord
end
end
+ initializer "active_record.database_selector" do
+ if options = config.active_record.delete(:database_selector)
+ resolver = config.active_record.delete(:database_resolver)
+ operations = config.active_record.delete(:database_resolver_context)
+ config.app_middleware.use ActiveRecord::Middleware::DatabaseSelector, resolver, operations, options
+ end
+ end
+
initializer "Check for cache versioning support" do
config.after_initialize do |app|
ActiveSupport.on_load(:active_record) do
@@ -189,6 +197,7 @@ end_error
# and then establishes the connection.
initializer "active_record.initialize_database" do
ActiveSupport.on_load(:active_record) do
+ self.connection_handlers = { writing_role => ActiveRecord::Base.default_connection_handler }
self.configurations = Rails.application.config.database_configuration
establish_connection
end
diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
index b5129e4239..d57680aaaa 100644
--- a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
+++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
@@ -3,7 +3,7 @@
module ActiveRecord
module Railties # :nodoc:
module CollectionCacheAssociationLoading #:nodoc:
- def setup(context, options, block)
+ def setup(context, options, as, block)
@relation = relation_from_options(options)
super
@@ -20,12 +20,12 @@ module ActiveRecord
end
end
- def collection_without_template
+ def collection_without_template(*)
@relation.preload_associations(@collection) if @relation
super
end
- def collection_with_template
+ def collection_with_template(*)
@relation.preload_associations(@collection) if @relation
super
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index b2110f727c..6d2f75a3ae 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -612,21 +612,9 @@ module ActiveRecord
# returns either +nil+ or the inverse association name that it finds.
def automatic_inverse_of
- return unless can_find_inverse_of_automatically?(self)
+ if can_find_inverse_of_automatically?(self)
+ inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
- inverse_name_candidates =
- if options[:as]
- [options[:as]]
- else
- active_record_name = active_record.name.demodulize
- [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)]
- end
-
- inverse_name_candidates.map! do |candidate|
- ActiveSupport::Inflector.underscore(candidate).to_sym
- end
-
- inverse_name_candidates.detect do |inverse_name|
begin
reflection = klass._reflect_on_association(inverse_name)
rescue NameError
@@ -635,7 +623,9 @@ module ActiveRecord
reflection = false
end
- valid_inverse_reflection?(reflection)
+ if valid_inverse_reflection?(reflection)
+ return inverse_name
+ end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index a863227276..347d745d19 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -67,6 +67,7 @@ module ActiveRecord
# user = users.new { |user| user.name = 'Oscar' }
# user.name # => Oscar
def new(attributes = nil, &block)
+ block = klass.current_scope_restoring_block(&block)
scoping { klass.new(attributes, &block) }
end
@@ -92,7 +93,11 @@ module ActiveRecord
# users.create(name: nil) # validation on name
# # => #<User id: nil, name: nil, ...>
def create(attributes = nil, &block)
- scoping { klass.create(attributes, &block) }
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create(attr, &block) }
+ else
+ new(attributes, &block).tap(&:save)
+ end
end
# Similar to #create, but calls
@@ -102,7 +107,11 @@ module ActiveRecord
# Expects arguments in the same format as
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
def create!(attributes = nil, &block)
- scoping { klass.create!(attributes, &block) }
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create!(attr, &block) }
+ else
+ new(attributes, &block).tap(&:save!)
+ end
end
def first_or_create(attributes = nil, &block) # :nodoc:
@@ -312,12 +321,12 @@ module ActiveRecord
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
def scoping
- @delegate_to_klass ? yield : klass._scoping(self) { yield }
+ @delegate_to_klass && klass.current_scope(true) ? yield : _scoping(self) { yield }
end
def _exec_scope(*args, &block) # :nodoc:
@delegate_to_klass = true
- instance_exec(*args, &block) || self
+ _scoping(nil) { instance_exec(*args, &block) || self }
ensure
@delegate_to_klass = false
end
@@ -632,6 +641,13 @@ module ActiveRecord
end
private
+ def _scoping(scope)
+ previous, klass.current_scope = klass.current_scope(true), scope
+ yield
+ ensure
+ klass.current_scope = previous
+ end
+
def _substitute_values(values)
values.map do |name, value|
attr = arel_attribute(name)
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index cef31bea94..bdd3c540bb 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -186,11 +186,9 @@ module ActiveRecord
relation = apply_join_dependency
relation.pluck(*column_names)
else
- disallow_raw_sql!(column_names)
+ klass.disallow_raw_sql!(column_names)
relation = spawn
- relation.select_values = column_names.map { |cn|
- @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
- }
+ relation.select_values = column_names
result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil) }
result.cast_values(klass.attribute_types)
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 6e8a1fcad4..f7c3b3783f 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -132,7 +132,7 @@ module ActiveRecord
private
def respond_to_missing?(method, _)
- super || @klass.respond_to?(method) || arel.respond_to?(method)
+ super || @klass.respond_to?(method)
end
end
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 8f1065c1e7..91cfc4e849 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -307,8 +307,6 @@ module ActiveRecord
return false if !conditions || limit_value == 0
- conditions = sanitize_forbidden_attributes(conditions)
-
if eager_loading?
relation = apply_join_dependency(eager_loading: false)
return relation.exists?(conditions)
@@ -316,7 +314,7 @@ module ActiveRecord
relation = construct_relation_for_exists(conditions)
- skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false
+ skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists") } ? true : false
end
# This method is called whenever no records are found with either a single
@@ -354,7 +352,13 @@ module ActiveRecord
end
def construct_relation_for_exists(conditions)
- relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
+ conditions = sanitize_forbidden_attributes(conditions)
+
+ if distinct_value && offset_value
+ relation = limit(1)
+ else
+ relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
+ end
case conditions
when Array, Hash
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index 5e0b4ac160..1dd6462d8d 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -32,7 +32,7 @@ module ActiveRecord
if defined?(@_unboundable)
@_unboundable
else
- value_for_database
+ value_for_database unless value_before_type_cast.is_a?(StatementCache::Substitute)
@_unboundable = nil
end
rescue ::RangeError
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index eb80aab701..75976aa8fc 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1052,11 +1052,13 @@ module ActiveRecord
def arel_columns(columns)
columns.flat_map do |field|
- if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value
- arel_attribute(field)
- elsif Symbol === field
- connection.quote_table_name(field.to_s)
- elsif Proc === field
+ case field
+ when Symbol
+ field = field.to_s
+ arel_column(field) { connection.quote_table_name(field) }
+ when String
+ arel_column(field) { field }
+ when Proc
field.call
else
field
@@ -1064,6 +1066,16 @@ module ActiveRecord
end
end
+ def arel_column(field)
+ field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+
+ if klass.columns_hash.key?(field) && !from_clause.value
+ arel_attribute(field)
+ else
+ yield
+ end
+ end
+
def reverse_sql_order(order_query)
if order_query.empty?
return [arel_attribute(primary_key).desc] if primary_key
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 7874c4c35a..8cf4fc469f 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -8,7 +8,7 @@ module ActiveRecord
module SpawnMethods
# This is overridden by Associations::CollectionProxy
def spawn #:nodoc:
- @delegate_to_klass ? klass.all : clone
+ @delegate_to_klass && klass.current_scope(true) ? klass.all : clone
end
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 9eba1254a4..1142a87d25 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -23,14 +23,21 @@ module ActiveRecord
current_scope
end
- private
- def current_scope(skip_inherited_scope = false)
- ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
- end
+ def current_scope(skip_inherited_scope = false)
+ ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
+ end
- def current_scope=(scope)
- ScopeRegistry.set_value_for(:current_scope, self, scope)
+ def current_scope=(scope)
+ ScopeRegistry.set_value_for(:current_scope, self, scope)
+ end
+
+ def current_scope_restoring_block(&block)
+ current_scope = self.current_scope(true)
+ -> *args do
+ self.current_scope = current_scope
+ yield(*args) if block_given?
end
+ end
end
def populate_with_current_scope_attributes # :nodoc:
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 6caf9b3251..8c612df27a 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -31,14 +31,7 @@ module ActiveRecord
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
def unscoped
- block_given? ? _scoping(relation) { yield } : relation
- end
-
- def _scoping(relation) # :nodoc:
- previous, self.current_scope = current_scope(true), relation
- yield
- ensure
- self.current_scope = previous
+ block_given? ? relation.scoping { yield } : relation
end
# Are there attributes associated with this scope?
diff --git a/activerecord/lib/active_record/test_fixtures.rb b/activerecord/lib/active_record/test_fixtures.rb
index d29fc9f84b..8c60d71669 100644
--- a/activerecord/lib/active_record/test_fixtures.rb
+++ b/activerecord/lib/active_record/test_fixtures.rb
@@ -122,7 +122,7 @@ module ActiveRecord
# Begin transactions for connections already established
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
- connection.begin_transaction joinable: false
+ connection.begin_transaction joinable: false, _lazy: false
connection.pool.lock_thread = true if lock_threads
end
@@ -138,7 +138,7 @@ module ActiveRecord
end
if connection && !@fixture_connections.include?(connection)
- connection.begin_transaction joinable: false
+ connection.begin_transaction joinable: false, _lazy: false
connection.pool.lock_thread = true if lock_threads
@fixture_connections << connection
end
diff --git a/activerecord/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb
index c530a77bfb..bf516db35f 100644
--- a/activerecord/lib/arel/nodes/and.rb
+++ b/activerecord/lib/arel/nodes/and.rb
@@ -2,7 +2,7 @@
module Arel # :nodoc: all
module Nodes
- class And < Arel::Nodes::Node
+ class And < Arel::Nodes::NodeExpression
attr_reader :children
def initialize(children)
diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb
index b8f83128c8..1c4b727bf6 100644
--- a/activerecord/lib/arel/nodes/case.rb
+++ b/activerecord/lib/arel/nodes/case.rb
@@ -2,9 +2,7 @@
module Arel # :nodoc: all
module Nodes
- class Case < Arel::Nodes::Node
- include Arel::AliasPredication
-
+ class Case < Arel::Nodes::NodeExpression
attr_accessor :case, :conditions, :default
def initialize(expression = nil, default = nil)
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 05d8aa59c4..2baf3db49a 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -348,6 +348,10 @@ module ActiveRecord
assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
end
+ def test_supports_foreign_keys_in_create_is_deprecated
+ assert_deprecated { @connection.supports_foreign_keys_in_create? }
+ end
+
def test_supports_multi_insert_is_deprecated
assert_deprecated { @connection.supports_multi_insert? }
end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index 2d71ee2f15..88c2ac5d0a 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -139,8 +139,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
with_real_execute do
ActiveRecord::Base.connection.create_table :delete_me
ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
- assert column_present?("delete_me", "updated_at", "datetime")
- assert column_present?("delete_me", "created_at", "datetime")
+ assert column_exists?("delete_me", "updated_at", "datetime")
+ assert column_exists?("delete_me", "created_at", "datetime")
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
@@ -152,8 +152,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
t.timestamps null: true
end
ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true
- assert_not column_present?("delete_me", "updated_at", "datetime")
- assert_not column_present?("delete_me", "created_at", "datetime")
+ assert_not column_exists?("delete_me", "updated_at", "datetime")
+ assert_not column_exists?("delete_me", "created_at", "datetime")
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
@@ -194,9 +194,4 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
-
- def column_present?(table_name, column_name, type)
- results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'")
- results.first && results.first["Type"] == type
- end
end
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index f05dcac7dd..9d88b14dab 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -116,8 +116,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
def test_timestamps_without_null_set_null_to_false_on_change_table
@@ -129,8 +129,23 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps, bulk: true do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
end
def test_timestamps_without_null_set_null_to_false_on_add_timestamps
@@ -139,7 +154,58 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
add_timestamps :has_timestamps, default: Time.now
end
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ if subsecond_precision_supported?
+ def test_timestamps_sets_presicion_on_create_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+
+ def test_timestamps_sets_presicion_on_change_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_sets_presicion_on_change_table_with_bulk
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps, bulk: true do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+ end
+
+ def test_timestamps_sets_presicion_on_add_timestamps
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+ add_timestamps :has_timestamps, default: Time.now
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
end
end
diff --git a/activerecord/test/cases/arel/nodes/and_test.rb b/activerecord/test/cases/arel/nodes/and_test.rb
index eff54abd91..d123ca9fd0 100644
--- a/activerecord/test/cases/arel/nodes/and_test.rb
+++ b/activerecord/test/cases/arel/nodes/and_test.rb
@@ -16,6 +16,15 @@ module Arel
assert_equal 2, array.uniq.size
end
end
+
+ describe "functions as node expression" do
+ it "allows aliasing" do
+ aliased = And.new(["foo", "bar"]).as("baz")
+
+ assert_kind_of As, aliased
+ assert_kind_of SqlLiteral, aliased.right
+ 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 a61569420e..3525fa2ab8 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -63,7 +63,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
Client.find(3).firm
ensure
- assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query"
+ sql_log = ActiveRecord::SQLCounter.log
+ assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
end
def test_belongs_to_with_primary_key
@@ -447,8 +448,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_with_select
- assert_equal 1, Company.find(2).firm_with_select.attributes.size
- assert_equal 1, Company.all.merge!(includes: :firm_with_select).find(2).firm_with_select.attributes.size
+ assert_equal 1, Post.find(2).author_with_select.attributes.size
+ assert_equal 1, Post.includes(:author_with_select).find(2).author_with_select.attributes.size
+ end
+
+ def test_custom_attribute_with_select
+ assert_equal 2, Company.find(2).firm_with_select.attributes.size
+ assert_equal 2, Company.includes(:firm_with_select).find(2).firm_with_select.attributes.size
end
def test_belongs_to_without_counter_cache_option
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index a9e22c7643..b9e16cab21 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -18,7 +18,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
:categorizations, :people, :categories, :edges, :vertices
def test_eager_association_loading_with_cascaded_two_levels
- authors = Author.all.merge!(includes: { posts: :comments }, order: "authors.id").to_a
+ authors = Author.includes(posts: :comments).order(:id).to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
@@ -26,7 +26,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
- authors = Author.all.merge!(includes: [{ posts: :comments }, :categorizations], order: "authors.id").to_a
+ authors = Author.includes({ posts: :comments }, :categorizations).order(:id).to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
@@ -36,7 +36,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
- authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a
+ authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).order(:id).to_a
assert_equal 3, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 2f090d9862..bf44be1811 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -996,9 +996,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_predicate companies(:first_firm).clients_of_firm, :loaded?
- companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
+ result = companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
assert_equal 4, companies(:first_firm).clients_of_firm.size
assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
+ assert_equal companies(:first_firm).clients_of_firm, result
end
def test_transactions_when_adding_to_persisted
@@ -1224,12 +1225,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_has_many_without_counter_cache_option
# Ship has a conventionally named `treasures_count` column, but the counter_cache
# option is not given on the association.
- ship = Ship.create(name: "Countless", treasures_count: 10)
+ ship = Ship.create!(name: "Countless", treasures_count: 10)
assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
# Count should come from sql count() of treasures rather than treasures_count attribute
- assert_equal ship.treasures.size, 0
+ assert_queries(1) do
+ assert_equal ship.treasures.size, 0
+ assert_predicate ship.treasures, :loaded?
+ end
assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do
ship.treasures.create(name: "Gold")
@@ -1350,6 +1354,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
assert_no_queries do
assert_not_empty post.comments
+ assert_equal 2, post.comments.size
+ assert_not_predicate post.comments, :loaded?
+ end
+ post = posts(:misc_by_bob)
+ assert_no_queries do
+ assert_empty post.comments
+ assert_predicate post.comments, :loaded?
+ end
+ end
+
+ def test_empty_association_loading_with_counter_cache
+ post = posts(:misc_by_bob)
+ assert_no_queries do
+ assert_empty post.comments.to_a
end
end
@@ -1986,9 +2004,37 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_not_predicate company.clients, :loaded?
end
- def test_counter_cache_on_unloaded_association
- car = Car.create(name: "My AppliCar")
- assert_equal car.engines.size, 0
+ def test_ids_reader_cache_not_used_for_size_when_association_is_dirty
+ firm = Firm.create!(name: "Startup")
+ assert_equal 0, firm.client_ids.size
+ firm.clients.build
+ assert_equal 1, firm.clients.size
+ end
+
+ def test_ids_reader_cache_should_be_cleared_when_collection_is_deleted
+ firm = companies(:first_firm)
+ assert_equal [2, 3, 11], firm.client_ids
+ client = firm.clients.first
+ firm.clients.delete(client)
+ assert_equal [3, 11], firm.client_ids
+ end
+
+ def test_zero_counter_cache_usage_on_unloaded_association
+ car = Car.create!(name: "My AppliCar")
+ assert_no_queries do
+ assert_equal car.engines.size, 0
+ assert_predicate car.engines, :loaded?
+ end
+ end
+
+ def test_counter_cache_on_new_record_unloaded_association
+ car = Car.new(name: "My AppliCar")
+ # Ensure no schema queries inside assertion
+ Engine.primary_key
+ assert_no_queries do
+ assert_equal car.engines.size, 0
+ assert_predicate car.engines, :loaded?
+ end
end
def test_get_ids_ignores_include_option
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index bd535357ee..0ac56c6168 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -46,6 +46,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create person_id: 0, post_id: 0
end
+ def test_has_many_through_create_record
+ assert books(:awdr).subscribers.create!(nick: "bob")
+ end
+
def test_marshal_dump
preloaded = Post.includes(:first_blue_tags).first
assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
@@ -238,9 +242,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_concat
person = people(:david)
post = posts(:thinking)
- post.people.concat [person]
+ result = post.people.concat [person]
assert_equal 1, post.people.size
assert_equal 1, post.people.reload.size
+ assert_equal post.people, result
end
def test_associate_existing_record_twice_should_add_to_target_twice
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 3e5b5c1275..7bb629466d 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -37,8 +37,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
companies(:first_firm).account
ensure
- log_all = ActiveRecord::SQLCounter.log_all
- assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}"
+ sql_log = ActiveRecord::SQLCounter.log
+ assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
end
def test_has_one_cache_nils
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index eb4dc73423..da3a42e2b5 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -20,8 +20,6 @@ require "models/company"
require "models/project"
require "models/author"
require "models/post"
-require "models/department"
-require "models/hotel"
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
@@ -726,16 +724,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
# fails because Interest does have the correct inverse_of
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first }
end
-
- def test_favors_has_one_associations_for_inverse_of
- inverse_name = Post.reflect_on_association(:author).inverse_of.name
- assert_equal :post, inverse_name
- end
-
- def test_finds_inverse_of_for_plural_associations
- inverse_name = Department.reflect_on_association(:hotel).inverse_of.name
- assert_equal :departments, inverse_name
- end
end
# NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 4938b6865f..1b8f748bad 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1226,14 +1226,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attribute_names
- assert_equal ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"],
- Company.attribute_names
+ expected = ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description", "metadata"]
+ assert_equal expected, Company.attribute_names
end
def test_has_attribute
assert Company.has_attribute?("id")
assert Company.has_attribute?("type")
assert Company.has_attribute?("name")
+ assert Company.has_attribute?("metadata")
assert_not Company.has_attribute?("lastname")
assert_not Company.has_attribute?("age")
end
@@ -1535,7 +1536,7 @@ class BasicsTest < ActiveRecord::TestCase
Bird.create!(name: "Bluejay")
ActiveRecord::Base.connection.while_preventing_writes do
- assert_queries(2) { Bird.where(name: "Bluejay").explain }
+ assert_nothing_raised { Bird.where(name: "Bluejay").explain }
end
end
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
index 3a06b1c795..c27eb8a65d 100644
--- a/activerecord/test/cases/cache_key_test.rb
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -51,7 +51,7 @@ module ActiveRecord
end
test "cache_version is the same when it comes from the DB or from the user" do
- skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
@@ -63,7 +63,7 @@ module ActiveRecord
end
test "cache_version does not truncate zeros when timestamp ends in zeros" do
- skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
travel_to Time.now.beginning_of_day do
record = CacheMeWithVersion.create
@@ -84,7 +84,7 @@ module ActiveRecord
end
test "cache_version does NOT call updated_at when value is from the database" do
- skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter)
+ skip("Mysql2 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
record = CacheMeWithVersion.create
record_from_db = CacheMeWithVersion.find(record.id)
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 850bc49676..b001667ac9 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -690,8 +690,9 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_not_auto_table_name_prefix_if_column_joined
- Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
- assert_equal [7], Company.joins(:contracts).pluck(:developer_id)
+ company = Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
+ metadata = company.contracts.first.metadata
+ assert_equal [metadata], Company.joins(:contracts).pluck(:metadata)
end
def test_pluck_with_selection_clause
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 51d0cc3d12..6282759a10 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -382,6 +382,11 @@ module ActiveRecord
assert_not_nil ActiveRecord::Base.connection
assert_same klass2.connection, ActiveRecord::Base.connection
end
+
+ def test_default_handlers_are_writing_and_reading
+ assert_equal :writing, ActiveRecord::Base.writing_role
+ assert_equal :reading, ActiveRecord::Base.reading_role
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
index 865aacc1b5..36591097b6 100644
--- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
@@ -126,6 +126,30 @@ module ActiveRecord
ENV["RAILS_ENV"] = previous_env
end
+ def test_establish_connection_using_3_levels_config_with_non_default_handlers
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" },
+ "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.connects_to(database: { default: :primary, readonly: :readonly })
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:default].retrieve_connection_pool("primary")
+ assert_equal "db/primary.sqlite3", pool.spec.config[:database]
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:readonly].retrieve_connection_pool("primary")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
def test_switching_connections_with_database_url
previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo"
@@ -336,13 +360,31 @@ module ActiveRecord
end
def test_calling_connected_to_on_a_non_existent_handler_raises
- error = assert_raises ArgumentError do
+ error = assert_raises ActiveRecord::ConnectionNotEstablished do
ActiveRecord::Base.connected_to(role: :reading) do
- yield
+ Person.first
end
end
- assert_equal "The reading role does not exist. Add it by establishing a connection with `connects_to` or use an existing role (writing).", error.message
+ assert_equal "No connection pool with 'primary' found for the 'reading' role.", error.message
+ end
+
+ def test_default_handlers_are_writing_and_reading
+ assert_equal :writing, ActiveRecord::Base.writing_role
+ assert_equal :reading, ActiveRecord::Base.reading_role
+ end
+
+ def test_an_application_can_change_the_default_handlers
+ old_writing = ActiveRecord::Base.writing_role
+ old_reading = ActiveRecord::Base.reading_role
+ ActiveRecord::Base.writing_role = :default
+ ActiveRecord::Base.reading_role = :readonly
+
+ assert_equal :default, ActiveRecord::Base.writing_role
+ assert_equal :readonly, ActiveRecord::Base.reading_role
+ ensure
+ ActiveRecord::Base.writing_role = old_writing
+ ActiveRecord::Base.reading_role = old_reading
end
end
end
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 06c1c51724..225cccc62c 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
@@ -46,6 +46,14 @@ module ActiveRecord
assert_equal expected, actual
end
+ def test_resolver_with_nil_database_url_and_current_env
+ ENV["RAILS_ENV"] = "foo"
+ config = { "foo" => { "adapter" => "postgres", "url" => ENV["DATABASE_URL"] } }
+ actual = resolve_spec(:foo, config)
+ expected = { "adapter" => "postgres", "url" => nil, "name" => "foo" }
+ assert_equal expected, actual
+ end
+
def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env
ENV["DATABASE_URL"] = "postgres://localhost/foo"
ENV["RACK_ENV"] = "foo"
diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb
new file mode 100644
index 0000000000..fd02d2acb4
--- /dev/null
+++ b/activerecord/test/cases/database_selector_test.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/person"
+require "action_dispatch"
+
+module ActiveRecord
+ class DatabaseSelectorTest < ActiveRecord::TestCase
+ setup do
+ @session_store = {}
+ @session = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
+ end
+
+ teardown do
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
+ def test_empty_session
+ assert_equal Time.at(0), @session.last_write_timestamp
+ end
+
+ def test_writing_the_session_timestamps
+ assert @session.update_last_write_timestamp
+
+ session2 = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
+ assert_equal @session.last_write_timestamp, session2.last_write_timestamp
+ end
+
+ def test_writing_session_time_changes
+ assert @session.update_last_write_timestamp
+
+ before = @session.last_write_timestamp
+ sleep(0.1)
+
+ assert @session.update_last_write_timestamp
+ assert_not_equal before, @session.last_write_timestamp
+ end
+
+ def test_read_from_replicas
+ @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now - 5.seconds)
+
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ called = false
+ resolver.read do
+ called = true
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ end
+ assert called
+ end
+
+ def test_read_from_primary
+ @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now)
+
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ called = false
+ resolver.read do
+ called = true
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ end
+ assert called
+ end
+
+ def test_write_to_primary
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ # 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]
+ end
+
+ def test_write_to_primary_with_exception
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ assert_raises(ActiveRecord::RecordNotFound) do
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ raise ActiveRecord::RecordNotFound
+ end
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+ end
+
+ def test_read_from_primary_with_options
+ 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
+ resolver.read do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ read = true
+ end
+ assert read
+ end
+
+ def test_read_from_replica_with_no_delay
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 0.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
+ resolver.read do
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ read = true
+ end
+ assert read
+ end
+
+ def test_the_middleware_chooses_writing_role_with_POST_request
+ middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ [200, {}, ["body"]]
+ })
+ assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "POST")
+ end
+
+ def test_the_middleware_chooses_reading_role_with_GET_request
+ middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ [200, {}, ["body"]]
+ })
+ assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET")
+ end
+ end
+end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 6af2a43c7f..4040682280 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -271,6 +271,16 @@ class FinderTest < ActiveRecord::TestCase
assert_equal true, Topic.exists?({})
end
+ def test_exists_with_distinct_and_offset_and_joins
+ assert Post.left_joins(:comments).distinct.offset(10).exists?
+ assert_not Post.left_joins(:comments).distinct.offset(11).exists?
+ end
+
+ def test_exists_with_distinct_and_offset_and_select
+ assert Post.select(:body).distinct.offset(3).exists?
+ assert_not Post.select(:body).distinct.offset(4).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 2fe4879fe6..b4f28fbfd6 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -924,7 +924,7 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase
def lock_thread=(lock_thread); end
end.new
- assert_called_with(connection, :begin_transaction, [joinable: false]) do
+ assert_called_with(connection, :begin_transaction, [joinable: false, _lazy: false]) do
fire_connection_notification(connection)
end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 7777508349..cc0587fa50 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -462,7 +462,11 @@ module ActiveRecord
end
def test_create_table_with_force_cascade_drops_dependent_objects
- skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
+ skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE"
+ elsif current_adapter?(:SQLite3Adapter)
+ skip "SQLite3 does not support DROP TABLE CASCADE syntax"
+ end
# can't re-create table referenced by foreign key
assert_raises(ActiveRecord::StatementInvalid) do
@connection.create_table :trains, force: true
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index cedd9c44e3..1c66c8186f 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -318,6 +318,17 @@ module ActiveRecord
ensure
connection.drop_table(:my_table) rescue nil
end
+
+ def test_add_column_without_column_name
+ e = assert_raise ArgumentError do
+ connection.create_table "my_table", force: true do |t|
+ t.timestamp
+ end
+ end
+ assert_equal "Missing column name(s) for timestamp", e.message
+ ensure
+ connection.drop_table :my_table, if_exists: true
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
index 017ee7951e..5753bd7117 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -86,8 +86,8 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:more_testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:more_testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:more_testings, :created_at, null: true)
+ assert connection.column_exists?(:more_testings, :updated_at, null: true)
ensure
connection.drop_table :more_testings rescue nil
end
@@ -103,8 +103,25 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table_with_bulk
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def migrate(x)
+ change_table :testings, bulk: true do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
end
def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table
@@ -116,8 +133,70 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
+
+ def test_timestamps_doesnt_set_precision_on_create_table
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ create_table :more_testings do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:more_testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:more_testings, :updated_at, null: false, **precision_implicit_default)
+ ensure
+ connection.drop_table :more_testings rescue nil
+ end
+
+ def test_timestamps_doesnt_set_precision_on_change_table
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ change_table :testings do |t|
+ t.timestamps default: Time.now
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_doesnt_set_precision_on_change_table_with_bulk
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ change_table :testings, bulk: true do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
+ end
+
+ def test_timestamps_doesnt_set_precision_on_add_timestamps
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ add_timestamps :testings, default: Time.now
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
end
def test_legacy_migrations_raises_exception_when_inherited
@@ -159,6 +238,15 @@ module ActiveRecord
ActiveRecord::Base.clear_cache!
end
end
+
+ private
+ def precision_implicit_default
+ if current_adapter?(:Mysql2Adapter)
+ { presicion: 0 }
+ else
+ { presicion: nil }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index bb233fbf74..32586e525b 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -3,7 +3,7 @@
require "cases/helper"
require "support/schema_dumping_helper"
-if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
+if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ForeignKeyInCreateTest < ActiveRecord::TestCase
@@ -20,9 +20,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
end
end
- class ForeignKeyChangeColumnTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
-
+ module ForeignKeyChangeColumnSharedTest
class Rocket < ActiveRecord::Base
has_many :astronauts
end
@@ -31,24 +29,39 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
belongs_to :rocket
end
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.create_table "rockets", force: true do |t|
- t.string :name
+ class CreateRocketsMigration < ActiveRecord::Migration::Current
+ def up
+ create_table :rockets do |t|
+ t.string :name
+ end
+
+ create_table :astronauts do |t|
+ t.string :name
+ t.references :rocket, foreign_key: true
+ end
end
- @connection.create_table "astronauts", force: true do |t|
- t.string :name
- t.references :rocket, foreign_key: true
+ def down
+ drop_table :astronauts, if_exists: true
+ drop_table :rockets, if_exists: true
end
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @migration = CreateRocketsMigration.new
+ silence_stream($stdout) { @migration.migrate(:up) }
+ Rocket.reset_table_name
Rocket.reset_column_information
+ Astronaut.reset_table_name
Astronaut.reset_column_information
end
- teardown do
- @connection.drop_table "astronauts", if_exists: true
- @connection.drop_table "rockets", if_exists: true
+ def teardown
+ silence_stream($stdout) { @migration.migrate(:down) }
+ Rocket.reset_table_name
Rocket.reset_column_information
+ Astronaut.reset_table_name
Astronaut.reset_column_information
end
@@ -56,53 +69,114 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
rocket = Rocket.create!(name: "myrocket")
rocket.astronauts << Astronaut.create!
- @connection.change_column_null :rockets, :name, false
+ @connection.change_column_null Rocket.table_name, :name, false
- foreign_keys = @connection.foreign_keys("astronauts")
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
assert_equal 1, foreign_keys.size
fk = foreign_keys.first
assert_equal "myrocket", Rocket.first.name
- assert_equal "astronauts", fk.from_table
- assert_equal "rockets", fk.to_table
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
end
def test_rename_column_of_child_table
rocket = Rocket.create!(name: "myrocket")
rocket.astronauts << Astronaut.create!
- @connection.rename_column :astronauts, :name, :astronaut_name
+ @connection.rename_column Astronaut.table_name, :name, :astronaut_name
- foreign_keys = @connection.foreign_keys("astronauts")
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
assert_equal 1, foreign_keys.size
fk = foreign_keys.first
assert_equal "myrocket", Rocket.first.name
- assert_equal "astronauts", fk.from_table
- assert_equal "rockets", fk.to_table
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
end
def test_rename_reference_column_of_child_table
rocket = Rocket.create!(name: "myrocket")
rocket.astronauts << Astronaut.create!
- @connection.rename_column :astronauts, :rocket_id, :new_rocket_id
+ @connection.rename_column Astronaut.table_name, :rocket_id, :new_rocket_id
- foreign_keys = @connection.foreign_keys("astronauts")
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
assert_equal 1, foreign_keys.size
fk = foreign_keys.first
assert_equal "myrocket", Rocket.first.name
- assert_equal "astronauts", fk.from_table
- assert_equal "rockets", fk.to_table
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
assert_equal "new_rocket_id", fk.options[:column]
end
+
+ def test_remove_reference_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.remove_column Astronaut.table_name, :rocket_id
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+
+ def test_remove_foreign_key_by_column
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.remove_foreign_key Astronaut.table_name, column: :rocket_id
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+
+ def test_remove_foreign_key_by_column_in_change_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.change_table Astronaut.table_name do |t|
+ t.remove_foreign_key column: :rocket_id
+ end
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+ end
+
+ class ForeignKeyChangeColumnTest < ActiveRecord::TestCase
+ include ForeignKeyChangeColumnSharedTest
+
+ self.use_transactional_tests = false
+ end
+
+ class ForeignKeyChangeColumnWithPrefixTest < ActiveRecord::TestCase
+ include ForeignKeyChangeColumnSharedTest
+
+ self.use_transactional_tests = false
+
+ setup do
+ ActiveRecord::Base.table_name_prefix = "p_"
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_prefix = nil
+ end
+ end
+
+ class ForeignKeyChangeColumnWithSuffixTest < ActiveRecord::TestCase
+ include ForeignKeyChangeColumnSharedTest
+
+ self.use_transactional_tests = false
+
+ setup do
+ ActiveRecord::Base.table_name_suffix = "_p"
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_suffix = nil
+ end
end
end
end
-end
-if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ForeignKeyTest < ActiveRecord::TestCase
@@ -141,7 +215,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal "fk_test_has_pk", fk.to_table
assert_equal "fk_id", fk.column
assert_equal "pk_id", fk.primary_key
- assert_equal "fk_name", fk.name
+ assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter)
end
def test_add_foreign_key_inferes_column
@@ -155,7 +229,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal "rockets", fk.to_table
assert_equal "rocket_id", fk.column
assert_equal "id", fk.primary_key
- assert_equal("fk_rails_78146ddd2e", fk.name)
+ assert_equal "fk_rails_78146ddd2e", fk.name unless current_adapter?(:SQLite3Adapter)
end
def test_add_foreign_key_with_column
@@ -169,7 +243,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal "rockets", fk.to_table
assert_equal "rocket_id", fk.column
assert_equal "id", fk.primary_key
- assert_equal("fk_rails_78146ddd2e", fk.name)
+ assert_equal "fk_rails_78146ddd2e", fk.name unless current_adapter?(:SQLite3Adapter)
end
def test_add_foreign_key_with_non_standard_primary_key
@@ -188,7 +262,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal "space_shuttles", fk.to_table
assert_equal "pk", fk.primary_key
ensure
- @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ @connection.remove_foreign_key :astronauts, name: "custom_pk", to_table: "space_shuttles"
@connection.drop_table :space_shuttles
end
@@ -262,6 +336,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
def test_foreign_key_exists_by_name
+ skip if current_adapter?(:SQLite3Adapter)
+
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk")
@@ -293,6 +369,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
def test_remove_foreign_key_by_name
+ skip if current_adapter?(:SQLite3Adapter)
+
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
assert_equal 1, @connection.foreign_keys("astronauts").size
@@ -301,9 +379,10 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
def test_remove_foreign_non_existing_foreign_key_raises
- assert_raises ArgumentError do
+ e = assert_raises ArgumentError do
@connection.remove_foreign_key :astronauts, :rockets
end
+ assert_equal "Table 'astronauts' has no foreign key for rockets", e.message
end
if ActiveRecord::Base.connection.supports_validate_constraints?
@@ -382,7 +461,11 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
def test_schema_dumping_with_options
output = dump_table_schema "fk_test_has_fk"
- assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ if current_adapter?(:SQLite3Adapter)
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id"$}, output
+ else
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
end
def test_schema_dumping_with_custom_fk_ignore_pattern
@@ -436,7 +519,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current
- def change
+ def up
create_table(:schools)
create_table(:classes) do |t|
@@ -444,6 +527,11 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
add_foreign_key :classes, :schools
end
+
+ def down
+ drop_table :classes, if_exists: true
+ drop_table :schools, if_exists: true
+ end
end
def test_add_foreign_key_with_prefix
diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb
index 620e9ab6ca..0d9adcc145 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -2,7 +2,7 @@
require "cases/helper"
-if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
+if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase
@@ -65,9 +65,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
end
end
end
-end
-if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ReferencesForeignKeyTest < ActiveRecord::TestCase
@@ -172,13 +170,18 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
class CreateDogsMigration < ActiveRecord::Migration::Current
- def change
+ def up
create_table :dog_owners
create_table :dogs do |t|
t.references :dog_owner, foreign_key: true
end
end
+
+ def down
+ drop_table :dogs, if_exists: true
+ drop_table :dog_owners, if_exists: true
+ end
end
def test_references_foreign_key_with_prefix
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 46e2ff79d9..02031e51ef 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -626,6 +626,18 @@ class MigrationTest < ActiveRecord::TestCase
ensure
Person.connection.drop_table :test_text_limits, if_exists: true
end
+
+ def test_invalid_text_size_should_raise
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_text_sizes, force: true do |t|
+ t.text :bigtext, size: 0xfffffffff
+ end
+ end
+
+ assert_match(/#{0xfffffffff} is invalid :size value\. Only :tiny, :medium, and :long are allowed\./, e.message)
+ ensure
+ Person.connection.drop_table :test_text_sizes, if_exists: true
+ end
end
if ActiveRecord::Base.connection.supports_advisory_locks?
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 04bbc7d136..eb32b690aa 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -502,6 +502,44 @@ class QueryCacheTest < ActiveRecord::TestCase
}.call({})
end
+ def test_clear_query_cache_is_called_on_all_connections
+ skip "with in memory db, reading role won't be able to see database on writing role" if in_memory_db?
+ with_temporary_connection_pool do
+ ActiveRecord::Base.connection_handlers = {
+ writing: ActiveRecord::Base.default_connection_handler,
+ reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ }
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
+
+ mw = middleware { |env|
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @topic = Topic.first
+ end
+
+ assert @topic
+
+ ActiveRecord::Base.connected_to(role: :writing) do
+ @topic.title = "It doesn't have to be crazy at work"
+ @topic.save!
+ end
+
+ assert_equal "It doesn't have to be crazy at work", @topic.title
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @topic = Topic.first
+ assert_equal "It doesn't have to be crazy at work", @topic.title
+ end
+ }
+
+ mw.call({})
+ end
+ ensure
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
private
def with_temporary_connection_pool
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index b600c999a6..63ae438de3 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -5,7 +5,7 @@ require "models/post"
require "models/comment"
module ActiveRecord
- module ArrayDelegationTests
+ module DelegationTests
ARRAY_DELEGATES = [
:+, :-, :|, :&, :[], :shuffle,
:all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index,
@@ -21,10 +21,14 @@ module ActiveRecord
assert_respond_to target, method
end
end
+
+ def test_not_respond_to_arel_method
+ assert_not_respond_to target, :exists
+ end
end
class DelegationAssociationTest < ActiveRecord::TestCase
- include ArrayDelegationTests
+ include DelegationTests
def target
Post.new.comments
@@ -32,7 +36,7 @@ module ActiveRecord
end
class DelegationRelationTest < ActiveRecord::TestCase
- include ArrayDelegationTests
+ include DelegationTests
def target
Comment.all
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 756eeca35f..857d743605 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -14,6 +14,7 @@ require "models/person"
require "models/computer"
require "models/reply"
require "models/company"
+require "models/contract"
require "models/bird"
require "models/car"
require "models/engine"
@@ -480,21 +481,6 @@ class RelationTest < ActiveRecord::TestCase
assert_nothing_raised { Topic.reorder([]) }
end
- def test_respond_to_delegates_to_arel
- relation = Topic.all
- fake_arel = Struct.new(:responds) {
- def respond_to?(method, access = false)
- responds << [method, access]
- end
- }.new []
-
- relation.extend(Module.new { attr_accessor :arel })
- relation.arel = fake_arel
-
- relation.respond_to?(:matching_attributes)
- assert_equal [:matching_attributes, false], fake_arel.responds.first
- end
-
def test_respond_to_dynamic_finders
relation = Topic.all
@@ -1182,7 +1168,12 @@ class RelationTest < ActiveRecord::TestCase
end
def test_first_or_create_with_block
- parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" }
+ canary = Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_create do |bird|
+ bird.name = "parrot"
+ assert_equal canary, Bird.find_by!(name: "canary")
+ end
+ assert_equal 1, parrot.total_count
assert_kind_of Bird, parrot
assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
@@ -1224,7 +1215,12 @@ class RelationTest < ActiveRecord::TestCase
end
def test_first_or_create_bang_with_valid_block
- parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" }
+ canary = Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_create! do |bird|
+ bird.name = "parrot"
+ assert_equal canary, Bird.find_by!(name: "canary")
+ end
+ assert_equal 1, parrot.total_count
assert_kind_of Bird, parrot
assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
@@ -1274,7 +1270,12 @@ class RelationTest < ActiveRecord::TestCase
end
def test_first_or_initialize_with_block
- parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" }
+ canary = Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_initialize do |bird|
+ bird.name = "parrot"
+ assert_equal canary, Bird.find_by!(name: "canary")
+ end
+ assert_equal 1, parrot.total_count
assert_kind_of Bird, parrot
assert_not_predicate parrot, :persisted?
assert_predicate parrot, :valid?
@@ -1815,6 +1816,12 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [1, 1, 1], posts.map(&:author_address_id)
end
+ test "joins with select custom attribute" do
+ contract = Company.create!(name: "test").contracts.create!
+ company = Company.joins(:contracts).select(:id, :metadata).find(contract.company_id)
+ assert_equal contract.metadata, company.metadata
+ end
+
test "delegations do not leak to other classes" do
Topic.all.by_lifo
assert Topic.all.class.method_defined?(:by_lifo)
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index dda3efa47c..49e9be9565 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -245,25 +245,31 @@ class SchemaDumperTest < ActiveRecord::TestCase
if current_adapter?(:Mysql2Adapter)
def test_schema_dump_includes_length_for_mysql_binary_fields
- output = standard_dump
+ output = dump_table_schema "binary_fields"
assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output
assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output
end
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
- output = standard_dump
- assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output
+ output = dump_table_schema "binary_fields"
+ assert_match %r{t\.binary\s+"tiny_blob",\s+size: :tiny$}, output
assert_match %r{t\.binary\s+"normal_blob"$}, output
- assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output
- assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output
- assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output
+ assert_match %r{t\.binary\s+"medium_blob",\s+size: :medium$}, output
+ assert_match %r{t\.binary\s+"long_blob",\s+size: :long$}, output
+ assert_match %r{t\.text\s+"tiny_text",\s+size: :tiny$}, output
assert_match %r{t\.text\s+"normal_text"$}, output
- assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output
- assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
+ assert_match %r{t\.text\s+"medium_text",\s+size: :medium$}, output
+ assert_match %r{t\.text\s+"long_text",\s+size: :long$}, output
+ assert_match %r{t\.binary\s+"tiny_blob_2",\s+size: :tiny$}, output
+ assert_match %r{t\.binary\s+"medium_blob_2",\s+size: :medium$}, output
+ assert_match %r{t\.binary\s+"long_blob_2",\s+size: :long$}, output
+ assert_match %r{t\.text\s+"tiny_text_2",\s+size: :tiny$}, output
+ assert_match %r{t\.text\s+"medium_text_2",\s+size: :medium$}, output
+ assert_match %r{t\.text\s+"long_text_2",\s+size: :long$}, output
end
def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields
- output = standard_dump
+ output = dump_table_schema "booleans"
assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 418a2ae04e..27f9df295f 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -447,6 +447,16 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
end
+ def test_chaining_doesnt_leak_conditions_to_another_scopes
+ expected = Topic.where(approved: false).where(id: Topic.children.select(:parent_id))
+ assert_equal expected.to_a, Topic.rejected.has_children.to_a
+ end
+
+ def test_nested_scoping
+ expected = Reply.approved
+ assert_equal expected.to_a, Topic.rejected.nested_scoping(expected)
+ end
+
def test_scopes_batch_finders
assert_equal 4, Topic.approved.count
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index e3c12f68fd..6a6d73dc38 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -4,6 +4,7 @@ require "cases/helper"
require "models/book"
require "models/liquid"
require "models/molecule"
+require "models/numeric_data"
require "models/electron"
module ActiveRecord
@@ -74,6 +75,11 @@ module ActiveRecord
assert_equal "salty", liquids[0].name
end
+ def test_statement_cache_with_strictly_cast_attribute
+ row = NumericData.create(temperature: 1.5)
+ assert_equal row, NumericData.find_by(temperature: 1.5)
+ end
+
def test_statement_cache_values_differ
cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book")
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index 9eefc32745..f0a0e7f805 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -29,6 +29,14 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
end
end
+ def test_error_message_when_connection_not_established
+ error = assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.find(1)
+ end
+
+ assert_equal "No connection pool with 'primary' found.", error.message
+ end
+
def test_underlying_adapter_no_longer_active
assert_not @underlying.active?, "Removed adapter should no longer be active"
end
diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml
index 33962f9e5e..f5e3ac3c19 100644
--- a/activerecord/test/config.example.yml
+++ b/activerecord/test/config.example.yml
@@ -54,10 +54,16 @@ connections:
username: rails
encoding: utf8mb4
collation: utf8mb4_unicode_ci
+<% if ENV['MYSQL_HOST'] %>
+ host: <%= ENV['MYSQL_HOST'] %>
+<% end %>
arunit2:
username: rails
encoding: utf8mb4
collation: utf8mb4_general_ci
+<% if ENV['MYSQL_HOST'] %>
+ host: <%= ENV['MYSQL_HOST'] %>
+<% end %>
oracle:
arunit:
diff --git a/activerecord/test/models/bird.rb b/activerecord/test/models/bird.rb
index cfefa555b3..c9f6759c7d 100644
--- a/activerecord/test/models/bird.rb
+++ b/activerecord/test/models/bird.rb
@@ -16,4 +16,9 @@ class Bird < ActiveRecord::Base
def cancel_save_callback_method
throw(:abort)
end
+
+ attr_accessor :total_count
+ after_initialize do
+ self.total_count = Bird.count
+ end
end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 838f515aad..a0f48d23f1 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -13,6 +13,8 @@ class Company < AbstractCompany
has_many :contracts
has_many :developers, through: :contracts
+ attribute :metadata, :json
+
scope :of_first_firm, lambda {
joins(account: :firm).
where("firms.id" => 1)
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index 3f663375c4..89719775c4 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -5,7 +5,9 @@ class Contract < ActiveRecord::Base
belongs_to :developer, primary_key: :id
belongs_to :firm, foreign_key: "company_id"
- before_save :hi
+ attribute :metadata, :json
+
+ before_save :hi, :update_metadata
after_save :bye
attr_accessor :hi_count, :bye_count
@@ -19,6 +21,10 @@ class Contract < ActiveRecord::Base
@bye_count ||= 0
@bye_count += 1
end
+
+ def update_metadata
+ self.metadata = { company_id: company_id, developer_id: developer_id }
+ end
end
class NewContract < Contract
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index e32cc59399..65d1fce66d 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -31,6 +31,7 @@ class Post < ActiveRecord::Base
belongs_to :author_with_posts, -> { includes(:posts) }, class_name: "Author", foreign_key: :author_id
belongs_to :author_with_address, -> { includes(:author_address) }, class_name: "Author", foreign_key: :author_id
+ belongs_to :author_with_select, -> { select(:id) }, class_name: "Author", foreign_key: :author_id
def first_comment
super.body
diff --git a/activerecord/test/models/subscription.rb b/activerecord/test/models/subscription.rb
index d1d5d21621..f87315fcd1 100644
--- a/activerecord/test/models/subscription.rb
+++ b/activerecord/test/models/subscription.rb
@@ -3,4 +3,6 @@
class Subscription < ActiveRecord::Base
belongs_to :subscriber, counter_cache: :books_count
belongs_to :book
+
+ validates_presence_of :subscriber_id, :book_id
end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 03430154db..fdb461ed7f 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -10,19 +10,14 @@ class Topic < ActiveRecord::Base
scope :approved, -> { where(approved: true) }
scope :rejected, -> { where(approved: false) }
+ scope :children, -> { where.not(parent_id: nil) }
+ scope :has_children, -> { where(id: Topic.children.select(:parent_id)) }
+
scope :scope_with_lambda, lambda { all }
- scope :by_private_lifo, -> { where(author_name: private_lifo) }
scope :by_lifo, -> { where(author_name: "lifo") }
scope :replied, -> { where "replies_count > 0" }
- class << self
- private
- def private_lifo
- "lifo"
- end
- end
-
scope "approved_as_string", -> { where(approved: true) }
scope :anonymous_extension, -> { } do
def one
@@ -96,6 +91,10 @@ class Topic < ActiveRecord::Base
write_attribute(:approved, val)
end
+ def self.nested_scoping(scope)
+ scope.base
+ end
+
private
def default_written_on
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index 61e9bc9af7..b143035213 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -27,6 +27,7 @@ ActiveRecord::Schema.define do
create_table :binary_fields, force: true do |t|
t.binary :var_binary, limit: 255
t.binary :var_binary_large, limit: 4095
+
t.tinyblob :tiny_blob
t.blob :normal_blob
t.mediumblob :medium_blob
@@ -36,6 +37,13 @@ ActiveRecord::Schema.define do
t.mediumtext :medium_text
t.longtext :long_text
+ t.binary :tiny_blob_2, size: :tiny
+ t.binary :medium_blob_2, size: :medium
+ t.binary :long_blob_2, size: :long
+ t.text :tiny_text_2, size: :tiny
+ t.text :medium_text_2, size: :medium
+ t.text :long_text_2, size: :long
+
t.index :var_binary
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 7034c773d2..349b8afc48 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -247,6 +247,7 @@ ActiveRecord::Schema.define do
create_table :contracts, force: true do |t|
t.references :developer, index: false
t.references :company, index: false
+ t.string :metadata
end
create_table :customers, force: true do |t|
@@ -682,11 +683,7 @@ ActiveRecord::Schema.define do
create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- if subsecond_precision_supported?
- t.timestamps null: false, precision: 6
- else
- t.timestamps null: false
- end
+ t.timestamps
end
create_table :pets_treasures, force: true do |t|
@@ -904,11 +901,7 @@ ActiveRecord::Schema.define do
t.string :parent_title
t.string :type
t.string :group
- if subsecond_precision_supported?
- t.timestamps null: true, precision: 6
- else
- t.timestamps null: true
- end
+ t.timestamps null: true
end
create_table :toys, primary_key: :toy_id, force: true do |t|
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index 2a4fa53460..367309dd85 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -21,6 +21,7 @@ module ARTest
def self.connect
puts "Using #{connection_name}"
ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024)
+ ActiveRecord::Base.connection_handlers = { ActiveRecord::Base.writing_role => ActiveRecord::Base.default_connection_handler }
ActiveRecord::Base.configurations = connection_config
ActiveRecord::Base.establish_connection :arunit
ARUnit2Model.establish_connection :arunit2
diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md
index 659bacebde..0c7e0426ae 100644
--- a/activestorage/CHANGELOG.md
+++ b/activestorage/CHANGELOG.md
@@ -1,5 +1,11 @@
## Rails 6.0.0.beta1 (January 18, 2019) ##
+* [Rename npm package](https://github.com/rails/rails/pull/34905) from
+ [`activestorage`](https://www.npmjs.com/package/activestorage) to
+ [`@rails/activestorage`](https://www.npmjs.com/package/@rails/activestorage).
+
+ *Javan Makhmali*
+
* Replace `config.active_storage.queue` with two options that indicate which
queues analysis and purge jobs should use, respectively:
diff --git a/activestorage/README.md b/activestorage/README.md
index f658b8d542..2886169ca7 100644
--- a/activestorage/README.md
+++ b/activestorage/README.md
@@ -101,7 +101,7 @@ Variation of image attachment:
```erb
<%# Hitting the variant URL will lazy transform the original blob and then redirect to its new service location %>
-<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %>
+<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
```
## Direct uploads
diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb
index 03d5511481..32e8fcefdf 100644
--- a/activestorage/app/models/active_storage/blob/representable.rb
+++ b/activestorage/app/models/active_storage/blob/representable.rb
@@ -10,7 +10,7 @@ module ActiveStorage::Blob::Representable
# Returns an ActiveStorage::Variant instance with the set of +transformations+ provided. This is only relevant for image
# files, and it allows any image to be transformed for size, colors, and the like. Example:
#
- # avatar.variant(resize_to_fit: [100, 100]).processed.service_url
+ # avatar.variant(resize_to_limit: [100, 100]).processed.service_url
#
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100px.
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
@@ -18,7 +18,7 @@ module ActiveStorage::Blob::Representable
# Frequently, though, you don't actually want to transform the variant right away. But rather simply refer to a
# specific variant that can be created by a controller on-demand. Like so:
#
- # <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
+ # <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
#
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
# can then produce on-demand.
@@ -43,13 +43,13 @@ module ActiveStorage::Blob::Representable
# from a non-image blob. Active Storage comes with built-in previewers for videos and PDF documents. The video previewer
# extracts the first frame from a video and the PDF previewer extracts the first page from a PDF document.
#
- # blob.preview(resize_to_fit: [100, 100]).processed.service_url
+ # blob.preview(resize_to_limit: [100, 100]).processed.service_url
#
# Avoid processing previews synchronously in views. Instead, link to a controller action that processes them on demand.
# Active Storage provides one, but you may want to create your own (for example, if you need authentication). Here’s
# how to use the built-in version:
#
- # <%= image_tag video.preview(resize_to_fit: [100, 100]) %>
+ # <%= image_tag video.preview(resize_to_limit: [100, 100]) %>
#
# This method raises ActiveStorage::UnpreviewableError if no previewer accepts the receiving blob. To determine
# whether a blob is accepted by any previewer, call ActiveStorage::Blob#previewable?.
@@ -69,7 +69,7 @@ module ActiveStorage::Blob::Representable
# Returns an ActiveStorage::Preview for a previewable blob or an ActiveStorage::Variant for a variable image blob.
#
- # blob.representation(resize_to_fit: [100, 100]).processed.service_url
+ # blob.representation(resize_to_limit: [100, 100]).processed.service_url
#
# Raises ActiveStorage::UnrepresentableError if the receiving blob is neither variable nor previewable. Call
# ActiveStorage::Blob#representable? to determine whether a blob is representable.
diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb
index dd50494799..bb9d960443 100644
--- a/activestorage/app/models/active_storage/preview.rb
+++ b/activestorage/app/models/active_storage/preview.rb
@@ -38,7 +38,7 @@ class ActiveStorage::Preview
# Processes the preview if it has not been processed yet. Returns the receiving Preview instance for convenience:
#
- # blob.preview(resize_to_fit: [100, 100]).processed.service_url
+ # blob.preview(resize_to_limit: [100, 100]).processed.service_url
#
# Processing a preview generates an image from its blob and attaches the preview image to the blob. Because the preview
# image is stored with the blob, it is only generated once.
diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb
index ea57fa5f78..bc0058967a 100644
--- a/activestorage/app/models/active_storage/variant.rb
+++ b/activestorage/app/models/active_storage/variant.rb
@@ -27,7 +27,7 @@ require "ostruct"
# To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided
# by Active Storage like so:
#
-# <%= image_tag Current.user.avatar.variant(resize_to_fit: [100, 100]) %>
+# <%= image_tag Current.user.avatar.variant(resize_to_limit: [100, 100]) %>
#
# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController
# can then produce on-demand.
@@ -36,15 +36,15 @@ require "ostruct"
# has already been processed and uploaded to the service, and, if so, just return that. Otherwise it will perform
# the transformations, upload the variant to the service, and return itself again. Example:
#
-# avatar.variant(resize_to_fit: [100, 100]).processed.service_url
+# avatar.variant(resize_to_limit: [100, 100]).processed.service_url
#
# This will create and process a variant of the avatar blob that's constrained to a height and width of 100.
# Then it'll upload said variant to the service according to a derivative key of the blob and the transformations.
#
# You can combine any number of ImageMagick/libvips operations into a variant, as well as any macros provided by the
-# ImageProcessing gem (such as +resize_to_fit+):
+# ImageProcessing gem (such as +resize_to_limit+):
#
-# avatar.variant(resize_to_fit: [800, 800], monochrome: true, rotate: "-90")
+# avatar.variant(resize_to_limit: [800, 800], monochrome: true, rotate: "-90")
#
# Visit the following links for a list of available ImageProcessing commands and ImageMagick/libvips operations:
#
diff --git a/activestorage/app/models/active_storage/variation.rb b/activestorage/app/models/active_storage/variation.rb
index 3adc2407e5..67568772da 100644
--- a/activestorage/app/models/active_storage/variation.rb
+++ b/activestorage/app/models/active_storage/variation.rb
@@ -6,7 +6,7 @@
# In case you do need to use this directly, it's instantiated using a hash of transformations where
# the key is the command and the value is the arguments. Example:
#
-# ActiveStorage::Variation.new(resize_to_fit: [100, 100], monochrome: true, trim: true, rotate: "-90")
+# ActiveStorage::Variation.new(resize_to_limit: [100, 100], monochrome: true, trim: true, rotate: "-90")
#
# The options map directly to {ImageProcessing}[https://github.com/janko-m/image_processing] commands.
class ActiveStorage::Variation
diff --git a/activestorage/lib/active_storage/analyzer/video_analyzer.rb b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
index 18d8ff8237..e56c97f4f5 100644
--- a/activestorage/lib/active_storage/analyzer/video_analyzer.rb
+++ b/activestorage/lib/active_storage/analyzer/video_analyzer.rb
@@ -11,7 +11,7 @@ module ActiveStorage
#
# Example:
#
- # ActiveStorage::VideoAnalyzer.new(blob).metadata
+ # ActiveStorage::Analyzer::VideoAnalyzer.new(blob).metadata
# # => { width: 640.0, height: 480.0, duration: 5.0, angle: 0, display_aspect_ratio: [4, 3] }
#
# When a video's angle is 90 or 270 degrees, its width and height are automatically swapped for convenience.
diff --git a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
index 69eb617d7b..6bf501a607 100644
--- a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
+++ b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb
@@ -28,7 +28,7 @@ module ActiveStorage
private
def draw_first_page_from(file, &block)
- # use 72 dpi to match thumbnail dimesions of the PDF
+ # use 72 dpi to match thumbnail dimensions of the PDF
draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block
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 17cecd891c..993cc0e5f7 100644
--- a/activestorage/lib/active_storage/service/azure_storage_service.rb
+++ b/activestorage/lib/active_storage/service/azure_storage_service.rb
@@ -10,8 +10,8 @@ module ActiveStorage
class Service::AzureStorageService < Service
attr_reader :client, :blobs, :container, :signer
- def initialize(storage_account_name:, storage_access_key:, container:)
- @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key)
+ def initialize(storage_account_name:, storage_access_key:, container:, **options)
+ @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key, **options)
@signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key)
@blobs = client.blob_client
@container = container
diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb
index 382920ef61..bf94f3f49e 100644
--- a/activestorage/lib/active_storage/service/s3_service.rb
+++ b/activestorage/lib/active_storage/service/s3_service.rb
@@ -16,9 +16,9 @@ module ActiveStorage
@upload_options = upload
end
- def upload(key, io, checksum: nil, **)
+ def upload(key, io, checksum: nil, content_type: nil, **)
instrument :upload, key: key, checksum: checksum do
- object_for(key).put(upload_options.merge(body: io, content_md5: checksum))
+ object_for(key).put(upload_options.merge(body: io, content_md5: checksum, content_type: content_type))
rescue Aws::S3::Errors::BadDigest
raise ActiveStorage::IntegrityError
end
diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb
index 0a6004267f..74c0aa0405 100644
--- a/activestorage/test/service/s3_service_test.rb
+++ b/activestorage/test/service/s3_service_test.rb
@@ -59,6 +59,24 @@ if SERVICE_CONFIGURATIONS[:s3]
service.delete key
end
end
+
+ test "upload with content type" do
+ key = SecureRandom.base58(24)
+ data = "Something else entirely!"
+ content_type = "text/plain"
+
+ @service.upload(
+ key,
+ StringIO.new(data),
+ checksum: Digest::MD5.base64digest(data),
+ filename: "cool_data.txt",
+ content_type: content_type
+ )
+
+ assert_equal content_type, @service.bucket.object(key).content_type
+ ensure
+ @service.delete key
+ end
end
else
puts "Skipping S3 Service tests because no S3 configuration was supplied"
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 6272dd3471..8bb531f1b8 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,3 +1,25 @@
+* Revise `ActiveSupport::Notifications.unsubscribe` to correctly handle Regex or other multiple-pattern subscribers.
+
+ *Zach Kemp*
+
+* Add `before_reset` callback to `CurrentAttributes` and define `after_reset` as an alias of `resets` for symmetry.
+
+ *Rosa Gutierrez*
+
+* Remove the `` Kernel#` `` override that suppresses ENOENT and accidentally returns nil on Unix systems.
+
+ *Akinori Musha*
+
+* Add `ActiveSupport::HashWithIndifferentAccess#assoc`.
+
+ `assoc` can now be called with either a string or a symbol.
+
+ *Stefan Schüßler*
+
+* Add `Hash#deep_transform_values`, and `Hash#deep_transform_values!`.
+
+ *Guillermo Iguaran*
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
* Remove deprecated `Module#reachable?` method.
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 106b616529..629eb2dd70 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -62,13 +62,13 @@ module ActiveSupport
return if pruning?
@pruning = true
begin
- start_time = Time.now
+ start_time = Concurrent.monotonic_time
cleanup
instrument(:prune, target_size, from: @cache_size) do
keys = synchronize { @key_access.keys.sort { |a, b| @key_access[a].to_f <=> @key_access[b].to_f } }
keys.each do |key|
delete_entry(key, options)
- return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
+ return if @cache_size <= target_size || (max_time && Concurrent.monotonic_time - start_time > max_time)
end
end
ensure
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index c4b9e5f1a0..2f0901d853 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -2,6 +2,7 @@
require "active_support/core_ext/hash/conversions"
require "active_support/core_ext/hash/deep_merge"
+require "active_support/core_ext/hash/deep_transform_values"
require "active_support/core_ext/hash/except"
require "active_support/core_ext/hash/indifferent_access"
require "active_support/core_ext/hash/keys"
diff --git a/activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb b/activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb
new file mode 100644
index 0000000000..720a1f67c8
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/deep_transform_values.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+class Hash
+ # Returns a new hash with all keys converted by the block operation.
+ # This includes the keys from the root hash and from all
+ # nested hashes and arrays.
+ #
+ # hash = { person: { name: 'Rob', age: '28' } }
+ #
+ # hash.deep_transform_values{ |value| value.to_s.upcase }
+ # # => {person: {name: "ROB", age: "28"}}
+ def deep_transform_values(&block)
+ _deep_transform_values_in_object(self, &block)
+ end
+
+ # Destructively converts all values by using the block operation.
+ # This includes the values from the root hash and from all
+ # nested hashes and arrays.
+ def deep_transform_values!(&block)
+ _deep_transform_values_in_object!(self, &block)
+ end
+
+ private
+ # support methods for deep transforming nested hashes and arrays
+ def _deep_transform_values_in_object(object, &block)
+ case object
+ when Hash
+ object.transform_values { |value| _deep_transform_values_in_object(value, &block) }
+ when Array
+ object.map { |e| _deep_transform_values_in_object(e, &block) }
+ else
+ yield(object)
+ end
+ end
+
+ def _deep_transform_values_in_object!(object, &block)
+ case object
+ when Hash
+ object.transform_values! { |value| _deep_transform_values_in_object!(value, &block) }
+ when Array
+ object.map! { |e| _deep_transform_values_in_object!(e, &block) }
+ else
+ yield(object)
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb
index 0f4356fbdd..7708069301 100644
--- a/activesupport/lib/active_support/core_ext/kernel.rb
+++ b/activesupport/lib/active_support/core_ext/kernel.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/kernel/agnostics"
require "active_support/core_ext/kernel/concern"
require "active_support/core_ext/kernel/reporting"
require "active_support/core_ext/kernel/singleton_class"
diff --git a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb b/activesupport/lib/active_support/core_ext/kernel/agnostics.rb
deleted file mode 100644
index 403b5f31f0..0000000000
--- a/activesupport/lib/active_support/core_ext/kernel/agnostics.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-class Object
- # Makes backticks behave (somewhat more) similarly on all platforms.
- # On win32 `nonexistent_command` raises Errno::ENOENT; on Unix, the
- # spawned shell prints a message to stderr and sets $?. We emulate
- # Unix on the former but not the latter.
- def `(command) #:nodoc:
- super
- rescue Errno::ENOENT => e
- STDERR.puts "#$0: #{e}"
- end
-end
diff --git a/activesupport/lib/active_support/current_attributes.rb b/activesupport/lib/active_support/current_attributes.rb
index 3145ff87a1..67ebe102d7 100644
--- a/activesupport/lib/active_support/current_attributes.rb
+++ b/activesupport/lib/active_support/current_attributes.rb
@@ -119,10 +119,16 @@ module ActiveSupport
end
end
+ # Calls this block before #reset is called on the instance. Used for resetting external collaborators that depend on current values.
+ def before_reset(&block)
+ set_callback :reset, :before, &block
+ end
+
# Calls this block after #reset is called on the instance. Used for resetting external collaborators, like Time.zone.
def resets(&block)
set_callback :reset, :after, &block
end
+ alias_method :after_reset, :resets
delegate :set, :reset, to: :instance
diff --git a/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
new file mode 100644
index 0000000000..6892b31a5d
--- /dev/null
+++ b/activesupport/lib/active_support/dependencies/zeitwerk_integration.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+module ActiveSupport
+ module Dependencies
+ module ZeitwerkIntegration # :nodoc: all
+ module Decorations
+ def clear
+ Dependencies.unload_interlock do
+ Rails.autoloader.reload
+ end
+ end
+
+ def constantize(cpath)
+ Inflector.constantize(cpath)
+ end
+
+ def safe_constantize(cpath)
+ Inflector.safe_constantize(cpath)
+ end
+
+ def autoloaded_constants
+ Rails.autoloaders.flat_map do |autoloader|
+ autoloader.loaded.to_a
+ end
+ end
+
+ def autoloaded?(object)
+ cpath = object.is_a?(Module) ? object.name : object.to_s
+ Rails.autoloaders.any? { |autoloader| autoloader.loaded?(cpath) }
+ end
+ end
+
+ class << self
+ def take_over
+ setup_autoloaders
+ freeze_autoload_paths
+ decorate_dependencies
+ end
+
+ private
+
+ def setup_autoloaders
+ Dependencies.autoload_paths.each do |autoload_path|
+ if File.directory?(autoload_path)
+ if autoload_once?(autoload_path)
+ Rails.once_autoloader.push_dir(autoload_path)
+ else
+ Rails.autoloader.push_dir(autoload_path)
+ end
+ end
+ end
+
+ Rails.autoloaders.each(&:setup)
+ end
+
+ def autoload_once?(autoload_path)
+ Dependencies.autoload_once_paths.include?(autoload_path) ||
+ Gem.path.any? { |gem_path| autoload_path.to_s.start_with?(gem_path) }
+ end
+
+ def freeze_autoload_paths
+ Dependencies.autoload_paths.freeze
+ Dependencies.autoload_once_paths.freeze
+ end
+
+ def decorate_dependencies
+ Dependencies.singleton_class.prepend(Decorations)
+ Object.class_eval { alias_method :require_dependency, :require }
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/encrypted_file.rb b/activesupport/lib/active_support/encrypted_file.rb
index c66f1b557e..2b7db568a5 100644
--- a/activesupport/lib/active_support/encrypted_file.rb
+++ b/activesupport/lib/active_support/encrypted_file.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "pathname"
+require "tmpdir"
require "active_support/message_encryptor"
module ActiveSupport
@@ -67,7 +68,7 @@ module ActiveSupport
write(updated_contents) if updated_contents != contents
ensure
- FileUtils.rm(tmp_path) if tmp_path.exist?
+ FileUtils.rm(tmp_path) if tmp_path&.exist?
end
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index f1af76019a..3a2b2652c4 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -164,6 +164,19 @@ module ActiveSupport
super(convert_key(key))
end
+ # Same as <tt>Hash#assoc</tt> where the key passed as argument can be
+ # either a string or a symbol:
+ #
+ # counters = ActiveSupport::HashWithIndifferentAccess.new
+ # counters[:foo] = 1
+ #
+ # counters.assoc('foo') # => ["foo", 1]
+ # counters.assoc(:foo) # => ["foo", 1]
+ # counters.assoc(:zoo) # => nil
+ def assoc(key)
+ super(convert_key(key))
+ end
+
# Same as <tt>Hash#fetch</tt> where the key passed as argument can be
# either a string or a symbol:
#
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index 7ccc333463..d9e93b530c 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -153,6 +153,15 @@ module ActiveSupport
#
# ActiveSupport::Notifications.unsubscribe("render")
#
+ # Subscribers using a regexp or other pattern-matching object will remain subscribed
+ # to all events that match their original pattern, unless those events match a string
+ # passed to `unsubscribe`:
+ #
+ # subscriber = ActiveSupport::Notifications.subscribe(/render/) { }
+ # ActiveSupport::Notifications.unsubscribe('render_template.action_view')
+ # subscriber.matches?('render_template.action_view') # => false
+ # subscriber.matches?('render_partial.action_view') # => true
+ #
# == Default Queue
#
# Notifications ships with a queue implementation that consumes and publishes events
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index 4e4ca70942..f06504cf2c 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -2,6 +2,7 @@
require "mutex_m"
require "concurrent/map"
+require "set"
module ActiveSupport
module Notifications
@@ -13,7 +14,8 @@ module ActiveSupport
include Mutex_m
def initialize
- @subscribers = []
+ @string_subscribers = Hash.new { |h, k| h[k] = [] }
+ @other_subscribers = []
@listeners_for = Concurrent::Map.new
super
end
@@ -21,8 +23,13 @@ module ActiveSupport
def subscribe(pattern = nil, block = Proc.new)
subscriber = Subscribers.new pattern, block
synchronize do
- @subscribers << subscriber
- @listeners_for.clear
+ if String === pattern
+ @string_subscribers[pattern] << subscriber
+ @listeners_for.delete(pattern)
+ else
+ @other_subscribers << subscriber
+ @listeners_for.clear
+ end
end
subscriber
end
@@ -31,12 +38,19 @@ module ActiveSupport
synchronize do
case subscriber_or_name
when String
- @subscribers.reject! { |s| s.matches?(subscriber_or_name) }
+ @string_subscribers[subscriber_or_name].clear
+ @listeners_for.delete(subscriber_or_name)
+ @other_subscribers.each { |sub| sub.unsubscribe!(subscriber_or_name) }
else
- @subscribers.delete(subscriber_or_name)
+ pattern = subscriber_or_name.try(:pattern)
+ if String === pattern
+ @string_subscribers[pattern].delete(subscriber_or_name)
+ @listeners_for.delete(pattern)
+ else
+ @other_subscribers.delete(subscriber_or_name)
+ @listeners_for.clear
+ end
end
-
- @listeners_for.clear
end
end
@@ -56,7 +70,8 @@ module ActiveSupport
# this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics)
@listeners_for[name] || synchronize do
# use synchronisation when accessing @subscribers
- @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) }
+ @listeners_for[name] ||=
+ @string_subscribers[name] + @other_subscribers.select { |s| s.subscribed_to?(name) }
end
end
@@ -100,9 +115,33 @@ module ActiveSupport
end
end
+ class Matcher #:nodoc:
+ attr_reader :pattern, :exclusions
+
+ def self.wrap(pattern)
+ return pattern if String === pattern
+ new(pattern)
+ end
+
+ def initialize(pattern)
+ @pattern = pattern
+ @exclusions = Set.new
+ end
+
+ def unsubscribe!(name)
+ exclusions << -name if pattern === name
+ end
+
+ def ===(name)
+ pattern === name && !exclusions.include?(name)
+ end
+ end
+
class Evented #:nodoc:
+ attr_reader :pattern
+
def initialize(pattern, delegate)
- @pattern = pattern
+ @pattern = Matcher.wrap(pattern)
@delegate = delegate
@can_publish = delegate.respond_to?(:publish)
end
@@ -122,11 +161,15 @@ module ActiveSupport
end
def subscribed_to?(name)
- @pattern === name
+ pattern === name
end
def matches?(name)
- @pattern && @pattern === name
+ pattern && pattern === name
+ end
+
+ def unsubscribe!(name)
+ pattern.unsubscribe!(name)
end
end
@@ -189,6 +232,10 @@ module ActiveSupport
true
end
+ def unsubscribe!(*)
+ false
+ end
+
alias :matches? :===
end
end
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index 125c06f37a..00a57c38c9 100644
--- a/activesupport/lib/active_support/notifications/instrumenter.rb
+++ b/activesupport/lib/active_support/notifications/instrumenter.rb
@@ -137,7 +137,7 @@ module ActiveSupport
private
def now
- Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ Concurrent.monotonic_time
end
if clock_gettime_supported?
diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb
index 305a2c184d..273b196458 100644
--- a/activesupport/test/cache/stores/redis_cache_store_test.rb
+++ b/activesupport/test/cache/stores/redis_cache_store_test.rb
@@ -204,7 +204,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests
class RedisDistributedConnectionPoolBehaviourTest < ConnectionPoolBehaviourTest
private
def store_options
- { url: %w[ redis://localhost:6379/0 redis://localhost:6379/0 ] }
+ { url: [ENV["REDIS_URL"] || "redis://localhost:6379/0"] * 2 }
end
end
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index e8e0a1ae72..8572d56722 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -33,6 +33,8 @@ class HashExtTest < ActiveSupport::TestCase
h = {}
assert_respond_to h, :deep_transform_keys
assert_respond_to h, :deep_transform_keys!
+ assert_respond_to h, :deep_transform_values
+ assert_respond_to h, :deep_transform_values!
assert_respond_to h, :symbolize_keys
assert_respond_to h, :symbolize_keys!
assert_respond_to h, :deep_symbolize_keys
@@ -78,6 +80,31 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed)
end
+ def test_deep_transform_values
+ assert_equal({ "a" => "1", "b" => "2" }, @strings.deep_transform_values { |value| value.to_s })
+ assert_equal({ "a" => { "b" => { "c" => "3" } } }, @nested_strings.deep_transform_values { |value| value.to_s })
+ assert_equal({ "a" => [ { "b" => "2" }, { "c" => "3" }, "4" ] }, @string_array_of_hashes.deep_transform_values { |value| value.to_s })
+ end
+
+ def test_deep_transform_values_not_mutates
+ transformed_hash = @nested_mixed.deep_dup
+ transformed_hash.deep_transform_values { |value| value.to_s }
+ assert_equal @nested_mixed, transformed_hash
+ end
+
+ def test_deep_transform_values!
+ assert_equal({ "a" => "1", "b" => "2" }, @strings.deep_transform_values! { |value| value.to_s })
+ assert_equal({ "a" => { "b" => { "c" => "3" } } }, @nested_strings.deep_transform_values! { |value| value.to_s })
+ assert_equal({ "a" => [ { "b" => "2" }, { "c" => "3" }, "4" ] }, @string_array_of_hashes.deep_transform_values! { |value| value.to_s })
+ end
+
+ def test_deep_transform_values_with_bang_mutates
+ transformed_hash = @nested_mixed.deep_dup
+ transformed_hash.deep_transform_values! { |value| value.to_s }
+ assert_equal({ "a" => { b: { "c" => "3" } } }, transformed_hash)
+ assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed)
+ end
+
def test_symbolize_keys
assert_equal @symbols, @symbols.symbolize_keys
assert_equal @symbols, @strings.symbolize_keys
diff --git a/activesupport/test/current_attributes_test.rb b/activesupport/test/current_attributes_test.rb
index 1669f08f68..adbdc646bc 100644
--- a/activesupport/test/current_attributes_test.rb
+++ b/activesupport/test/current_attributes_test.rb
@@ -3,13 +3,18 @@
require "abstract_unit"
class CurrentAttributesTest < ActiveSupport::TestCase
- Person = Struct.new(:name, :time_zone)
+ Person = Struct.new(:id, :name, :time_zone)
class Current < ActiveSupport::CurrentAttributes
attribute :world, :account, :person, :request
delegate :time_zone, to: :person
- resets { Time.zone = "UTC" }
+ before_reset { Session.previous = person.try(:id) }
+
+ resets do
+ Time.zone = "UTC"
+ Session.current = nil
+ end
def account=(account)
super
@@ -19,6 +24,7 @@ class CurrentAttributesTest < ActiveSupport::TestCase
def person=(person)
super
Time.zone = person.try(:time_zone)
+ Session.current = person.try(:id)
end
def request
@@ -30,9 +36,14 @@ class CurrentAttributesTest < ActiveSupport::TestCase
end
end
+ class Session < ActiveSupport::CurrentAttributes
+ attribute :current, :previous
+ end
+
setup do
@original_time_zone = Time.zone
Current.reset
+ Session.reset
end
teardown do
@@ -56,16 +67,28 @@ class CurrentAttributesTest < ActiveSupport::TestCase
end
test "set auxiliary class via overwritten method" do
- Current.person = Person.new("David", "Central Time (US & Canada)")
+ Current.person = Person.new(42, "David", "Central Time (US & Canada)")
assert_equal "Central Time (US & Canada)", Time.zone.name
+ assert_equal 42, Session.current
end
- test "resets auxiliary class via callback" do
- Current.person = Person.new("David", "Central Time (US & Canada)")
+ test "resets auxiliary classes via callback" do
+ Current.person = Person.new(42, "David", "Central Time (US & Canada)")
assert_equal "Central Time (US & Canada)", Time.zone.name
Current.reset
assert_equal "UTC", Time.zone.name
+ assert_nil Session.current
+ end
+
+ test "set auxiliary class based on current attributes via before callback" do
+ Current.person = Person.new(42, "David", "Central Time (US & Canada)")
+ assert_nil Session.previous
+ assert_equal 42, Session.current
+
+ Current.reset
+ assert_equal 42, Session.previous
+ assert_nil Session.current
end
test "set attribute only via scope" do
@@ -92,13 +115,13 @@ class CurrentAttributesTest < ActiveSupport::TestCase
end
test "delegation" do
- Current.person = Person.new("David", "Central Time (US & Canada)")
+ Current.person = Person.new(42, "David", "Central Time (US & Canada)")
assert_equal "Central Time (US & Canada)", Current.time_zone
assert_equal "Central Time (US & Canada)", Current.instance.time_zone
end
test "all methods forward to the instance" do
- Current.person = Person.new("David", "Central Time (US & Canada)")
+ Current.person = Person.new(42, "David", "Central Time (US & Canada)")
assert_equal "David, in Central Time (US & Canada)", Current.intro
assert_equal "David, in Central Time (US & Canada)", Current.instance.intro
end
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index b1b3070891..d4e709137e 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -841,7 +841,7 @@ class DependenciesTest < ActiveSupport::TestCase
remove_constants(:C)
end
- def test_new_contants_in_without_constants
+ def test_new_constants_in_without_constants
assert_equal [], (ActiveSupport::Dependencies.new_constants_in(Object) { })
assert ActiveSupport::Dependencies.constant_watch_stack.all? { |k, v| v.empty? }
end
diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb
index f81e0dc70f..1d6a07ec5f 100644
--- a/activesupport/test/hash_with_indifferent_access_test.rb
+++ b/activesupport/test/hash_with_indifferent_access_test.rb
@@ -447,6 +447,14 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
end
+ def test_indifferent_assoc
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ key, value = indifferent_strings.assoc(:a)
+
+ assert_equal("a", key)
+ assert_equal(1, value)
+ end
+
def test_indifferent_compact
hash_contain_nil_value = @strings.merge("z" => nil)
hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value)
@@ -478,7 +486,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase
assert_equal @strings, roundtrip
assert_equal "1234", roundtrip.default
- # Ensure nested hashes are not HashWithIndiffereneAccess
+ # Ensure nested hashes are not HashWithIndifferentAccess
new_to_hash = @nested_mixed.with_indifferent_access.to_hash
assert_not new_to_hash.instance_of?(HashWithIndifferentAccess)
assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess)
diff --git a/activesupport/test/notifications/evented_notification_test.rb b/activesupport/test/notifications/evented_notification_test.rb
index 4beb8194b9..ab2a9b8659 100644
--- a/activesupport/test/notifications/evented_notification_test.rb
+++ b/activesupport/test/notifications/evented_notification_test.rb
@@ -84,6 +84,39 @@ module ActiveSupport
[:finish, "hi", 1, {}]
], listener.events
end
+
+ def test_listen_to_regexp
+ notifier = Fanout.new
+ listener = Listener.new
+ notifier.subscribe(/[a-z]*.world/, listener)
+ notifier.start("hi.world", 1, {})
+ notifier.finish("hi.world", 2, {})
+ notifier.start("hello.world", 1, {})
+ notifier.finish("hello.world", 2, {})
+
+ assert_equal [
+ [:start, "hi.world", 1, {}],
+ [:finish, "hi.world", 2, {}],
+ [:start, "hello.world", 1, {}],
+ [:finish, "hello.world", 2, {}]
+ ], listener.events
+ end
+
+ def test_listen_to_regexp_with_exclusions
+ notifier = Fanout.new
+ listener = Listener.new
+ notifier.subscribe(/[a-z]*.world/, listener)
+ notifier.unsubscribe("hi.world")
+ notifier.start("hi.world", 1, {})
+ notifier.finish("hi.world", 2, {})
+ notifier.start("hello.world", 1, {})
+ notifier.finish("hello.world", 2, {})
+
+ assert_equal [
+ [:start, "hello.world", 1, {}],
+ [:finish, "hello.world", 2, {}]
+ ], listener.events
+ end
end
end
end
diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb
index 4e0aef2cc7..bb20d26a25 100644
--- a/activesupport/test/notifications_test.rb
+++ b/activesupport/test/notifications_test.rb
@@ -81,7 +81,7 @@ module Notifications
assert_equal expected, events
end
- def test_subsribing_to_instrumentation_while_inside_it
+ def test_subscribing_to_instrumentation_while_inside_it
# the repro requires that there are no evented subscribers for the "foo" event,
# so we have to duplicate some of the setup code
old_notifier = ActiveSupport::Notifications.notifier
@@ -128,6 +128,25 @@ module Notifications
assert_equal [["named.subscription", :foo], ["named.subscription", :foo]], @events
end
+ def test_unsubscribing_by_name_leaves_regexp_matched_subscriptions
+ @matched_events = []
+ @notifier.subscribe(/subscription/) { |*args| @matched_events << event(*args) }
+ @notifier.publish("named.subscription", :before)
+ @notifier.wait
+ [@events, @named_events, @matched_events].each do |collector|
+ assert_includes(collector, ["named.subscription", :before])
+ end
+ @notifier.unsubscribe("named.subscription")
+ @notifier.publish("named.subscription", :after)
+ @notifier.publish("other.subscription", :after)
+ @notifier.wait
+ assert_includes(@events, ["named.subscription", :after])
+ assert_includes(@events, ["other.subscription", :after])
+ assert_includes(@matched_events, ["other.subscription", :after])
+ assert_not_includes(@matched_events, ["named.subscription", :after])
+ assert_not_includes(@named_events, ["named.subscription", :after])
+ end
+
private
def event(*args)
args
diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb
index 49a3951623..08d04e3223 100644
--- a/activesupport/test/safe_buffer_test.rb
+++ b/activesupport/test/safe_buffer_test.rb
@@ -151,7 +151,7 @@ class SafeBufferTest < ActiveSupport::TestCase
assert_equal "", ActiveSupport::SafeBuffer.new("foo").clone_empty
end
- test "clone_empty keeps the original dirtyness" do
+ test "clone_empty keeps the original dirtiness" do
assert_predicate @buffer.clone_empty, :html_safe?
assert_not_predicate @buffer.gsub!("", "").clone_empty, :html_safe?
end
diff --git a/activesupport/test/share_lock_test.rb b/activesupport/test/share_lock_test.rb
index 30a1ddad3f..a40c813fe3 100644
--- a/activesupport/test/share_lock_test.rb
+++ b/activesupport/test/share_lock_test.rb
@@ -38,7 +38,7 @@ class ShareLockTest < ActiveSupport::TestCase
end
end
- def test_multiple_exlusives_are_able_to_progress
+ def test_multiple_exclusives_are_able_to_progress
with_thread_waiting_in_lock_section(:sharing) do |sharing_thread_release_latch|
exclusive_threads = (1..2).map do
Thread.new do
diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb
index 9c61ab0ab5..a1f84bf69e 100644
--- a/activesupport/test/time_travel_test.rb
+++ b/activesupport/test/time_travel_test.rb
@@ -148,7 +148,7 @@ class TimeTravelTest < ActiveSupport::TestCase
end
end
- def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding
+ def test_travel_to_will_reset_the_usec_to_avoid_mysql_rounding
Time.stub(:now, Time.now) do
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do
assert_equal 50, Time.now.sec
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index 6d45a6726d..f948c363df 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -32,7 +32,7 @@ class TimeZoneTest < ActiveSupport::TestCase
end
end
- def test_period_for_local_with_ambigiuous_time
+ def test_period_for_local_with_ambiguous_time
zone = ActiveSupport::TimeZone["Moscow"]
period = zone.period_for_local(Time.utc(2015, 1, 1))
assert_equal period, zone.period_for_local(Time.utc(2014, 10, 26, 1, 0, 0))
diff --git a/ci/qunit-selenium-runner.rb b/ci/qunit-selenium-runner.rb
index 132b3d17eb..1df6aedb36 100644
--- a/ci/qunit-selenium-runner.rb
+++ b/ci/qunit-selenium-runner.rb
@@ -1,14 +1,20 @@
# frozen_string_literal: true
require "qunit/selenium/test_runner"
-require "chromedriver-helper"
-driver_options = Selenium::WebDriver::Chrome::Options.new
-driver_options.add_argument("--headless")
-driver_options.add_argument("--disable-gpu")
-driver_options.add_argument("--no-sandbox")
+if ARGV[1]
+ driver = ::Selenium::WebDriver.for(:remote, url: ARGV[1], desired_capabilities: :chrome)
+else
+ require "chromedriver-helper"
+
+ driver_options = Selenium::WebDriver::Chrome::Options.new
+ driver_options.add_argument("--headless")
+ driver_options.add_argument("--disable-gpu")
+ driver_options.add_argument("--no-sandbox")
+
+ driver = ::Selenium::WebDriver.for(:chrome, options: driver_options)
+end
-driver = ::Selenium::WebDriver.for(:chrome, options: driver_options)
result = QUnit::Selenium::TestRunner.new(driver).open(ARGV[0], timeout: 60)
driver.quit
diff --git a/guides/Rakefile b/guides/Rakefile
index 4116e6f9cc..b7425f6de4 100644
--- a/guides/Rakefile
+++ b/guides/Rakefile
@@ -88,4 +88,15 @@ HELP
end
end
+task :test do
+ templates = Dir.glob("bug_report_templates/*.rb")
+ counter = templates.count do |file|
+ puts "--- Running #{file}"
+ Bundler.clean_system(Gem.ruby, "-w", file) ||
+ puts("+++ 💥 FAILED (exit #{$?.exitstatus})")
+ end
+ puts "+++ #{counter} / #{templates.size} templates executed successfully"
+ exit 1 if counter < templates.size
+end
+
task default: "guides:help"
diff --git a/guides/assets/images/getting_started/template_is_missing_articles_new.png b/guides/assets/images/getting_started/template_is_missing_articles_new.png
index a1603f5d28..830b19bd9d 100644
--- a/guides/assets/images/getting_started/template_is_missing_articles_new.png
+++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png
Binary files differ
diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb
index d88304a219..74b329115d 100644
--- a/guides/bug_report_templates/active_record_gem.rb
+++ b/guides/bug_report_templates/active_record_gem.rb
@@ -9,7 +9,7 @@ gemfile(true) do
# Activate the gem you are reporting the issue against.
gem "activerecord", "5.2.0"
- gem "sqlite3"
+ gem "sqlite3", "~> 1.3.6"
end
require "active_record"
diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb
index 5dfd49fb38..8d71863034 100644
--- a/guides/bug_report_templates/active_record_migrations_gem.rb
+++ b/guides/bug_report_templates/active_record_migrations_gem.rb
@@ -9,7 +9,7 @@ gemfile(true) do
# Activate the gem you are reporting the issue against.
gem "activerecord", "5.2.0"
- gem "sqlite3"
+ gem "sqlite3", "~> 1.3.6"
end
require "active_record"
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index fd33c3f8a7..b0ddb0e7e0 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -150,8 +150,8 @@ module RailsGuides
puts "Generating #{guide} as #{output_file}"
layout = @kindle ? "kindle/layout" : "layout"
- view = ActionView::Base.new(
- @source_dir,
+ view = ActionView::Base.with_view_paths(
+ [@source_dir],
edge: @edge,
version: @version,
mobi: "kindle/#{mobi}",
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index aa746e4731..d0d84251e4 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -212,7 +212,7 @@ class PeopleController < ActionController::Base
end
# This will pass with flying colors as long as there's a person key
- # in the parameters, otherwise it'll raise a
+ # in the parameters, otherwise it'll raise an
# ActionController::ParameterMissing exception, which will get
# caught by ActionController::Base and turned into a 400 Bad
# Request error.
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
index 905c76e5c1..2c1796c464 100644
--- a/guides/source/active_record_migrations.md
+++ b/guides/source/active_record_migrations.md
@@ -496,9 +496,6 @@ NOTE: Active Record only supports single column foreign keys. `execute` and
`structure.sql` are required to use composite foreign keys. See
[Schema Dumping and You](#schema-dumping-and-you).
-NOTE: The SQLite3 adapter doesn't support `add_foreign_key` since SQLite supports
-only [a limited subset of ALTER TABLE](https://www.sqlite.org/lang_altertable.html).
-
Removing a foreign key is easy as well:
```ruby
diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md
index 474a93c83e..e3bb41ae32 100644
--- a/guides/source/active_storage_overview.md
+++ b/guides/source/active_storage_overview.md
@@ -434,7 +434,7 @@ original blob into the specified format and redirect to its new service
location.
```erb
-<%= image_tag user.avatar.variant(resize_to_fit: [100, 100]) %>
+<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
```
To switch to the Vips processor, you would add the following to
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 3db46bc42e..903f39994f 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -1201,6 +1201,8 @@ The `inquiry` method converts a string into a `StringInquirer` object making equ
"active".inquiry.inactive? # => false
```
+NOTE: Defined in `active_support/core_ext/string/inquiry.rb`.
+
### `starts_with?` and `ends_with?`
Active Support defines 3rd person aliases of `String#start_with?` and `String#end_with?`:
diff --git a/guides/source/api_app.md b/guides/source/api_app.md
index 85367c50e7..870f5f7b87 100644
--- a/guides/source/api_app.md
+++ b/guides/source/api_app.md
@@ -374,7 +374,7 @@ controller modules by default:
- `ActionController::Renderers::All`: Support for `render :json` and friends.
- `ActionController::ConditionalGet`: Support for `stale?`.
- `ActionController::BasicImplicitRender`: Makes sure to return an empty response, if there isn't an explicit one.
-- `ActionController::StrongParameters`: Support for parameters white-listing in combination with Active Model mass assignment.
+- `ActionController::StrongParameters`: Support for parameters filtering in combination with Active Model mass assignment.
- `ActionController::DataStreaming`: Support for `send_file` and `send_data`.
- `AbstractController::Callbacks`: Support for `before_action` and
similar helpers.
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index e7faa5c330..9f8edea598 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -745,7 +745,7 @@ mapping requests back to Sprockets. A typical manifest file looks like:
The default location for the manifest is the root of the location specified in
`config.assets.prefix` ('/assets' by default).
-NOTE: If there are missing precompiled files in production you will get an
+NOTE: If there are missing precompiled files in production you will get a
`Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError`
exception indicating the name of the missing file(s).
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index c799f6c01e..ad82614a0d 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -590,13 +590,6 @@ Defaults to `'signed cookie'`.
* `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`.
-* `config.action_view.finalize_compiled_template_methods` determines
- whether the methods on `ActionView::CompiledTemplates` that templates
- compile themselves to are removed when template instances are
- destroyed by the garbage collector. This helps prevent memory leaks in
- development mode, but for large test suites, disabling this option in
- the test environment can improve performance. This defaults to `true`.
-
### Configuring Action Mailbox
@@ -1242,6 +1235,8 @@ Using Initializer Files
After loading the framework and any gems in your application, Rails turns to loading initializers. An initializer is any Ruby file stored under `config/initializers` in your application. You can use initializers to hold configuration settings that should be made after all of the frameworks and gems are loaded, such as options to configure settings for these parts.
+NOTE: There is no guarantee that your initializers will run after all the gem initializers, so any initialization code that depends on a given gem having been initialized should go into a `config.after_initialize` block.
+
NOTE: You can use subfolders to organize your initializers if you like, because Rails will look into the whole file hierarchy from the initializers folder on down.
TIP: While Rails supports numbering of initializer file names for load ordering purposes, a better technique is to place any code that need to load in a specific order within the same file. This reduces file name churn, makes dependencies more explicit, and can help surface new concepts within your application.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index f15383e3f1..053e3aa16e 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -1497,6 +1497,8 @@ To hook into the initialization process of one of the following classes use the
| Class | Available Hooks |
| --------------------------------- | ------------------------------------ |
| `ActionCable` | `action_cable` |
+| `ActionCable::Channel::Base` | `action_cable_channel` |
+| `ActionCable::Connection::Base` | `action_cable_connection` |
| `ActionController::API` | `action_controller_api` |
| `ActionController::API` | `action_controller` |
| `ActionController::Base` | `action_controller_base` |
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 264c94326e..be59cd0cfa 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -461,22 +461,19 @@ available, Rails will raise an exception.
Let's look at the full error message again:
->ArticlesController#new is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot.
+>ArticlesController#new is missing a template for request formats: text/html
-That's quite a lot of text! Let's quickly go through and understand what each
-part of it means.
+>NOTE!
+>Unless told otherwise, Rails expects an action to render a template with the same name, contained in a folder named after its controller. If this controller is an API responding with 204 (No Content), which does not require a template, then this error will occur when trying to access it via browser, since we expect an HTML template to be rendered for such requests. If that's the case, carry on.
-The first part identifies which template is missing. In this case, it's the
+The message identifies which template is missing. In this case, it's the
`articles/new` template. Rails will first look for this template. If not found,
-then it will attempt to load a template called `application/new`. It looks for
-one here because the `ArticlesController` inherits from `ApplicationController`.
-
-The next part of the message contains `request.formats` which specifies
-the format of template to be served in response. It is set to `text/html` as we
-requested this page via browser, so Rails is looking for an HTML template.
-`request.variant` specifies what kind of physical devices would be served by
-the response and helps Rails determine which template to use in the response.
-It is empty because no information has been provided.
+then it will attempt to load a template called `application/new`, because the
+`ArticlesController` inherits from `ApplicationController`.
+
+Next the message contains `request.formats` which specifies the format of
+template to be served in response. It is set to `text/html` as we requested
+this page via browser, so Rails is looking for an HTML template.
The simplest template that would work in this case would be one located at
`app/views/articles/new.html.erb`. The extension of this file name is important:
diff --git a/guides/source/routing.md b/guides/source/routing.md
index a33ac6a589..92d5b45e7d 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -260,7 +260,7 @@ In each of these cases, the named routes remain the same as if you did not use `
| PATCH/PUT | /admin/articles/:id | articles#update | article_path(:id) |
| DELETE | /admin/articles/:id | articles#destroy | article_path(:id) |
-TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo' => '/foo#index'`._
+TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo', to: '/foo#index'`._
### Nested Resources
diff --git a/guides/source/testing.md b/guides/source/testing.md
index f9661c52a0..9667521f3b 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -1806,16 +1806,16 @@ require "test_helper"
class WebNotificationsChannelTest < ActionCable::Channel::TestCase
test "subscribes and stream for user" do
- stub_connection current_user: users[:john]
+ stub_connection current_user: users(:john)
subscribe
- assert_has_stream_for users[:john]
+ assert_has_stream_for users(:john)
end
end
```
-See the API documentation for [`AcionCable::Channel::TestCase`](http://api.rubyonrails.org/classes/ActionCable/Channel/TestCase.html) for more information.
+See the API documentation for [`ActionCable::Channel::TestCase`](http://api.rubyonrails.org/classes/ActionCable/Channel/TestCase.html) for more information.
### Custom Assertions And Testing Broadcasts Inside Other Components
@@ -1837,6 +1837,33 @@ class ProductTest < ActionCable::TestCase
end
```
+If you want to test the broadcasting made with `Channel.broadcast_to`, you shoud use
+`Channel.broadcasting_for` to generate an underlying stream name:
+
+```ruby
+# app/jobs/chat_relay_job.rb
+class ChatRelayJob < ApplicationJob
+ def perform_later(room, message)
+ ChatChannel.broadcast_to room, text: message
+ end
+end
+
+# test/jobs/chat_relay_job_test.rb
+require 'test_helper'
+
+class ChatRelayJobTest < ActiveJob::TestCase
+ include ActionCable::TestHelper
+
+ test "broadcast message to room" do
+ room = rooms(:all)
+
+ assert_broadcast_on(ChatChannel.broadcasting_for(room), text: "Hi!") do
+ ChatRelayJob.perform_now(room, "Hi!")
+ end
+ end
+end
+```
+
Additional Testing Resources
----------------------------
diff --git a/package.json b/package.json
index 43055fa6bc..e5c71777ba 100644
--- a/package.json
+++ b/package.json
@@ -4,10 +4,7 @@
"actioncable",
"actiontext",
"activestorage",
- "actionview",
- "tmp/templates/app_template",
- "railties/test/fixtures/tmp/bukkits/**/test/dummy",
- "railties/test/fixtures/tmp/bukkits/**/spec/dummy"
+ "actionview"
],
"dependencies": {
"webpack": "^4.17.1"
diff --git a/railties/.gitignore b/railties/.gitignore
index c08562e016..17a49da08c 100644
--- a/railties/.gitignore
+++ b/railties/.gitignore
@@ -2,4 +2,5 @@
/test/500.html
/test/fixtures/tmp/
/test/initializer/root/log/
+/test/isolation/assets/yarn.lock
/tmp/
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 5b249de47e..d66add2ca0 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,3 +1,13 @@
+* Fix non-symbol access to nested hashes returned from `Rails::Application.config_for`
+ being broken by allowing non-symbol access with a deprecation notice.
+
+ *Ufuk Kayserilioglu*
+
+* Fix deeply nested namespace command printing.
+
+ *Gannon McGibbon*
+
+
## Rails 6.0.0.beta1 (January 18, 2019) ##
* Remove deprecated `after_bundle` helper inside plugins templates.
@@ -70,7 +80,7 @@
In other environments `Rails.application.config.hosts` is empty and no
`Host` header checks will be done. If you want to guard against header
- attacks on production, you have to manually whitelist the allowed hosts
+ attacks on production, you have to manually permit the allowed hosts
with:
Rails.application.config.hosts << "product.com"
@@ -83,7 +93,7 @@
# `beta1.product.com`.
Rails.application.config.hosts << /.*\.product\.com/
- A special case is supported that allows you to whitelist all sub-domains:
+ A special case is supported that allows you to permit all sub-domains:
# Allow requests from subdomains like `www.product.com` and
# `beta1.product.com`.
diff --git a/railties/Rakefile b/railties/Rakefile
index 445f6217b3..4ae546b202 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -36,10 +36,20 @@ namespace :test do
dirs = (ENV["TEST_DIR"] || ENV["TEST_DIRS"] || "**").split(",")
test_options = ENV["TESTOPTS"].to_s.split(/[\s]+/)
- test_files = dirs.map { |dir| "test/#{dir}/*_test.rb" }
- Dir[*test_files].each do |file|
- next true if file.start_with?("test/fixtures/")
+ test_patterns = dirs.map { |dir| "test/#{dir}/*_test.rb" }
+ test_files = Dir[*test_patterns].select do |file|
+ !file.start_with?("test/fixtures/") && !file.start_with?("test/isolation/assets/")
+ end.sort
+ if ENV["BUILDKITE_PARALLEL_JOB_COUNT"]
+ n = ENV["BUILDKITE_PARALLEL_JOB"].to_i
+ m = ENV["BUILDKITE_PARALLEL_JOB_COUNT"].to_i
+
+ test_files = test_files.each_slice(m).map { |slice| slice[n] }.compact
+ end
+
+ test_files.each do |file|
+ puts "--- #{file}"
fake_command = Shellwords.join([
FileUtils::RUBY,
"-w",
@@ -59,10 +69,13 @@ namespace :test do
unless $?.success?
failing_files << file
+ puts "^^^ +++"
end
end
+ puts "--- All tests completed"
unless failing_files.empty?
+ puts "^^^ +++"
puts
puts "Failed in:"
failing_files.each do |file|
@@ -70,7 +83,7 @@ namespace :test do
end
puts
- raise "Failure in isolated test runner"
+ exit 1
end
end
end
diff --git a/railties/lib/minitest/rails_plugin.rb b/railties/lib/minitest/rails_plugin.rb
index 6486fa1798..4b7df6360a 100644
--- a/railties/lib/minitest/rails_plugin.rb
+++ b/railties/lib/minitest/rails_plugin.rb
@@ -54,6 +54,6 @@ module Minitest
end
end
- # Backwardscompatibility with Rails 5.0 generated plugin test scripts
+ # Backwards compatibility with Rails 5.0 generated plugin test scripts
mattr_reader :run_via, default: {}
end
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index 092105d502..bca2cf34e1 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -110,5 +110,25 @@ module Rails
def public_path
application && Pathname.new(application.paths["public"].first)
end
+
+ def autoloader
+ if configuration.autoloader == :zeitwerk
+ @autoloader ||= Zeitwerk::Loader.new
+ end
+ end
+
+ def once_autoloader
+ if configuration.autoloader == :zeitwerk
+ @once_autoloader ||= Zeitwerk::Loader.new
+ end
+ end
+
+ def autoloaders
+ if configuration.autoloader == :zeitwerk
+ [autoloader, once_autoloader]
+ else
+ []
+ end
+ end
end
end
diff --git a/railties/lib/rails/app_loader.rb b/railties/lib/rails/app_loader.rb
index aabcc5970c..cc057a407d 100644
--- a/railties/lib/rails/app_loader.rb
+++ b/railties/lib/rails/app_loader.rb
@@ -23,7 +23,7 @@ control:
# too that you may or may not want (like yarn)
If you already have Rails binstubs in source control, you might be
-inadverently overwriting them during deployment by using bundle install
+inadvertently overwriting them during deployment by using bundle install
with the --binstubs option.
If your application was created prior to Rails 4, here's how to upgrade:
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index 5a924ab8e6..d0417f8a49 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -7,6 +7,7 @@ require "active_support/key_generator"
require "active_support/message_verifier"
require "active_support/encrypted_configuration"
require "active_support/deprecation"
+require "active_support/hash_with_indifferent_access"
require "rails/engine"
require "rails/secrets"
@@ -230,8 +231,8 @@ module Rails
config = YAML.load(ERB.new(yaml.read).result) || {}
config = (config["shared"] || {}).merge(config[env] || {})
- ActiveSupport::OrderedOptions.new.tap do |config_as_ordered_options|
- config_as_ordered_options.update(config.deep_symbolize_keys)
+ ActiveSupport::OrderedOptions.new.tap do |options|
+ options.update(NonSymbolAccessDeprecatedHash.new(config))
end
else
raise "Could not load configuration. No such file - #{yaml}"
@@ -590,5 +591,38 @@ module Rails
def build_middleware
config.app_middleware + super
end
+
+ class NonSymbolAccessDeprecatedHash < HashWithIndifferentAccess # :nodoc:
+ def initialize(value = nil)
+ if value.is_a?(Hash)
+ value.each_pair { |k, v| self[k] = v }
+ else
+ super
+ end
+ end
+
+ def []=(key, value)
+ if value.is_a?(Hash)
+ value = self.class.new(value)
+ end
+ super(key.to_sym, value)
+ end
+
+ private
+
+ def convert_key(key)
+ unless key.kind_of?(Symbol)
+ ActiveSupport::Deprecation.warn(<<~MESSAGE.squish)
+ Accessing hashes returned from config_for by non-symbol keys
+ is deprecated and will be removed in Rails 6.1.
+ Use symbols for access instead.
+ MESSAGE
+
+ key = key.to_sym
+ end
+
+ key
+ end
+ end
end
end
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index d5a66b6ec1..16fbc99e7a 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -20,7 +20,7 @@ module Rails
:read_encrypted_secrets, :log_level, :content_security_policy_report_only,
:content_security_policy_nonce_generator, :require_master_key, :credentials
- attr_reader :encoding, :api_only, :loaded_config_version
+ attr_reader :encoding, :api_only, :loaded_config_version, :autoloader
def initialize(*)
super
@@ -30,7 +30,7 @@ module Rails
@filter_parameters = []
@filter_redirect = []
@helpers_paths = []
- @hosts = Array(([IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0"), "localhost"] if Rails.env.development?))
+ @hosts = Array(([IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0"), ".localhost"] if Rails.env.development?))
@public_file_server = ActiveSupport::OrderedOptions.new
@public_file_server.enabled = true
@public_file_server.index_name = "index"
@@ -64,6 +64,7 @@ module Rails
@credentials = ActiveSupport::OrderedOptions.new
@credentials.content_path = default_credentials_content_path
@credentials.key_path = default_credentials_key_path
+ @autoloader = :classic
end
def load_defaults(target_version)
@@ -117,6 +118,8 @@ module Rails
when "6.0"
load_defaults "5.2"
+ self.autoloader = :zeitwerk if RUBY_ENGINE == "ruby"
+
if respond_to?(:action_view)
action_view.default_enforce_utf8 = false
end
@@ -267,6 +270,14 @@ module Rails
end
end
+ def autoloader=(autoloader)
+ if %i(classic zeitwerk).include?(autoloader)
+ @autoloader = autoloader
+ else
+ raise ArgumentError, "config.autoloader may be :classic or :zeitwerk, got #{autoloader.inspect} instead"
+ end
+ end
+
class Custom #:nodoc:
def initialize
@configurations = Hash.new
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 04aaf6dd9a..39e8ef6631 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -21,6 +21,13 @@ module Rails
end
end
+ initializer :let_zeitwerk_take_over do
+ if config.autoloader == :zeitwerk
+ require "active_support/dependencies/zeitwerk_integration"
+ ActiveSupport::Dependencies::ZeitwerkIntegration.take_over
+ end
+ end
+
initializer :add_builtin_route do |app|
if Rails.env.development?
app.routes.prepend do
@@ -66,6 +73,7 @@ module Rails
initializer :eager_load! do
if config.eager_load
ActiveSupport.run_load_hooks(:before_eager_load, self)
+ Zeitwerk::Loader.eager_load_all if defined?(Zeitwerk)
config.eager_load_namespaces.each(&:eager_load!)
end
end
diff --git a/railties/lib/rails/command/actions.rb b/railties/lib/rails/command/actions.rb
index cbb743346b..50651ad61a 100644
--- a/railties/lib/rails/command/actions.rb
+++ b/railties/lib/rails/command/actions.rb
@@ -11,10 +11,20 @@ module Rails
end
def require_application_and_environment!
+ require_application!
+ require_environment!
+ end
+
+ def require_application!
require ENGINE_PATH if defined?(ENGINE_PATH)
if defined?(APP_PATH)
require APP_PATH
+ end
+ end
+
+ def require_environment!
+ if defined?(APP_PATH)
Rails.application.require_environment!
end
end
diff --git a/railties/lib/rails/command/base.rb b/railties/lib/rails/command/base.rb
index 766872de8a..a22b198c66 100644
--- a/railties/lib/rails/command/base.rb
+++ b/railties/lib/rails/command/base.rb
@@ -115,7 +115,7 @@ module Rails
# For a Rails::Command::TestCommand placed in <tt>rails/command/test_command.rb</tt>
# would return <tt>rails/test</tt>.
def default_command_root
- path = File.expand_path(File.join("../commands", command_root_namespace), __dir__)
+ path = File.expand_path(relative_command_path, __dir__)
path if File.exist?(path)
end
@@ -135,12 +135,20 @@ module Rails
end
def command_root_namespace
- (namespace.split(":") - %w( rails )).first
+ (namespace.split(":") - %w(rails)).join(":")
+ end
+
+ def relative_command_path
+ File.join("../commands", *command_root_namespace.split(":"))
end
def namespaced_commands
commands.keys.map do |key|
- key == command_root_namespace ? key : "#{command_root_namespace}:#{key}"
+ if command_root_namespace.match?(/(\A|\:)#{key}\z/)
+ command_root_namespace
+ else
+ "#{command_root_namespace}:#{key}"
+ end
end
end
end
diff --git a/railties/lib/rails/command/behavior.rb b/railties/lib/rails/command/behavior.rb
index 7f32b04cf1..7fb2a99e67 100644
--- a/railties/lib/rails/command/behavior.rb
+++ b/railties/lib/rails/command/behavior.rb
@@ -71,8 +71,9 @@ module Rails
paths = []
namespaces.each do |namespace|
pieces = namespace.split(":")
- paths << pieces.dup.push(pieces.last).join("/")
- paths << pieces.join("/")
+ path = pieces.join("/")
+ paths << "#{path}/#{pieces.last}"
+ paths << path
end
paths.uniq!
paths
diff --git a/railties/lib/rails/commands/credentials/USAGE b/railties/lib/rails/commands/credentials/USAGE
index d235592f46..f7268a64d1 100644
--- a/railties/lib/rails/commands/credentials/USAGE
+++ b/railties/lib/rails/commands/credentials/USAGE
@@ -54,5 +54,5 @@ doesn't exist.
The encryption key can also be put in `ENV["RAILS_MASTER_KEY"]`, which takes
precedence over the file encryption key.
-In addition to that, the default credentials lookup paths can be overriden through
+In addition to that, the default credentials lookup paths can be overridden through
`config.credentials.content_path` and `config.credentials.key_path`.
diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb
index 852cd401d7..54ccd97506 100644
--- a/railties/lib/rails/commands/credentials/credentials_command.rb
+++ b/railties/lib/rails/commands/credentials/credentials_command.rb
@@ -20,7 +20,7 @@ module Rails
end
def edit
- require_application_and_environment!
+ require_application!
ensure_editor_available(command: "bin/rails credentials:edit") || (return)
@@ -37,7 +37,7 @@ module Rails
end
def show
- require_application_and_environment!
+ require_application!
say credentials.read.presence || missing_credentials_message
end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index f768c30db0..2485158a7b 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -472,11 +472,10 @@ module Rails
# Eager load the application by loading all ruby
# files inside eager_load paths.
def eager_load!
- config.eager_load_paths.each do |load_path|
- matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
- Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
- require_dependency file.sub(matcher, '\1')
- end
+ if Rails.autoloader
+ eager_load_with_zeitwerk!
+ else
+ eager_load_with_dependencies!
end
end
@@ -652,6 +651,22 @@ module Rails
private
+ def eager_load_with_zeitwerk!
+ (config.eager_load_paths - Zeitwerk::Loader.all_dirs).each do |path|
+ Dir.glob("#{path}/**/*.rb").sort.each { |file| require file }
+ end
+ end
+
+ def eager_load_with_dependencies!
+ config.eager_load_paths.each do |load_path|
+ # Starts after load_path plus a slash, ends before ".rb".
+ relname_range = (load_path.to_s.length + 1)...-3
+ Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
+ require_dependency file[relname_range]
+ end
+ end
+ end
+
def load_config_initializer(initializer) # :doc:
ActiveSupport::Notifications.instrument("load_config_initializer.railties", initializer: initializer) do
load(initializer)
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 3856a74a39..1a5f2ff203 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -222,6 +222,7 @@ module Rails
log :generate, what
options = args.extract_options!
+ options[:without_rails_env] = true
argument = args.flat_map(&:to_s).join(" ")
execute_command :rails, "generate #{what} #{argument}", options
@@ -284,14 +285,15 @@ module Rails
# based on the executor parameter provided.
def execute_command(executor, command, options = {}) # :doc:
log executor, command
- env = options[:env] || ENV["RAILS_ENV"] || "development"
+ env = options[:env] || ENV["RAILS_ENV"] || "development"
+ rails_env = " RAILS_ENV=#{env}" unless options[:without_rails_env]
sudo = options[:sudo] && !Gem.win_platform? ? "sudo " : ""
config = { verbose: false }
config[:capture] = options[:capture] if options[:capture]
config[:abort_on_failure] = options[:abort_on_failure] if options[:abort_on_failure]
- in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", config) }
+ in_root { run("#{sudo}#{extify(executor)} #{command}#{rails_env}", config) }
end
# Add an extension to the given name based on the platform.
diff --git a/railties/lib/rails/generators/database.rb b/railties/lib/rails/generators/database.rb
index be3e61bde8..18fc7be3ff 100644
--- a/railties/lib/rails/generators/database.rb
+++ b/railties/lib/rails/generators/database.rb
@@ -15,6 +15,7 @@ module Rails
case database
when "mysql" then ["mysql2", [">= 0.4.4"]]
when "postgresql" then ["pg", [">= 0.18", "< 2.0"]]
+ when "sqlite3" then ["sqlite3", ["~> 1.3", ">= 1.3.6"]]
when "oracle" then ["activerecord-oracle_enhanced-adapter", nil]
when "frontbase" then ["ruby-frontbase", nil]
when "sqlserver" then ["activerecord-sqlserver-adapter", nil]
diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb
index 5081060895..b6ec0160cf 100644
--- a/railties/lib/rails/generators/migration.rb
+++ b/railties/lib/rails/generators/migration.rb
@@ -63,8 +63,7 @@ module Rails
numbered_destination = File.join(dir, ["%migration_number%", base].join("_"))
create_migration numbered_destination, nil, config do
- match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /)
- if match && match[:version] >= "2.2.0" # Ruby 2.6+
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
ERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer").result(context)
else
ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context)
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
index d39b5d311f..a1f1224a45 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt
@@ -28,7 +28,7 @@ ruby <%= "'#{RUBY_VERSION}'" -%>
<% if depend_on_bootsnap? -%>
# Reduces boot times through caching; required in config/boot.rb
-gem 'bootsnap', '>= 1.1.0', require: false
+gem 'bootsnap', '>= 1.4.0', require: false
<%- end -%>
<%- if options.api? -%>
@@ -36,7 +36,9 @@ gem 'bootsnap', '>= 1.1.0', require: false
# gem 'rack-cors'
<%- end -%>
-<% if RUBY_ENGINE == 'ruby' -%>
+<% if RUBY_ENGINE == "ruby" -%>
+gem "zeitwerk", ">= 1.0.0"
+
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
index 08befd9196..ed1cf09eeb 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
@@ -98,4 +98,25 @@ Rails.application.configure do
# Do not dump schema after migrations.
config.active_record.dump_schema_after_migration = false
<%- end -%>
+
+ # Inserts middleware to perform automatic connection switching.
+ # The `database_selector` hash is used to pass options to the DatabaseSelector
+ # middleware. The `delay` is used to determine how long to wait after a write
+ # to send a subsequent read to the primary.
+ #
+ # The `database_resolver` class is used by the middleware to determine which
+ # database is appropriate to use based on the time delay.
+ #
+ # The `database_resolver_context` class is used by the middleware to set
+ # timestamps for the last write to the primary. The resolver uses the context
+ # class timestamps to determine how long to wait before reading from the
+ # replica.
+ #
+ # By default Rails will store a last write timestamp in the session. The
+ # DatabaseSelector middleware is designed as such you can define your own
+ # strategy for connection switching and pass that into the middleware through
+ # these configuration options.
+ # config.active_record.database_selector = { delay: 2.seconds }
+ # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
+ # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session
end
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 223aa56187..63ed3fa952 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
@@ -48,7 +48,4 @@ Rails.application.configure do
# Raises error for missing translations.
# config.action_view.raise_on_missing_translations = true
-
- # Prevent expensive template finalization at end of test suite runs.
- config.action_view.finalize_compiled_template_methods = false
end
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index 3e979ea20d..960f708bdf 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1157,6 +1157,27 @@ module ApplicationTests
end
end
+ test "autoloader & autoloader=" do
+ app "development"
+
+ config = Rails.application.config
+ assert_instance_of Zeitwerk::Loader, Rails.autoloader
+ assert_instance_of Zeitwerk::Loader, Rails.once_autoloader
+ assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders
+
+ config.autoloader = :classic
+ assert_nil Rails.autoloader
+ assert_nil Rails.once_autoloader
+ assert_empty Rails.autoloaders
+
+ config.autoloader = :zeitwerk
+ assert_instance_of Zeitwerk::Loader, Rails.autoloader
+ assert_instance_of Zeitwerk::Loader, Rails.once_autoloader
+ assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders
+
+ assert_raises(ArgumentError) { config.autoloader = :unknown }
+ end
+
test "config.action_view.cache_template_loading with cache_classes default" do
add_to_config "config.cache_classes = true"
@@ -1714,7 +1735,7 @@ module ApplicationTests
assert_equal true, Rails.application.config.action_mailer.show_previews
end
- test "config_for loads custom configuration from yaml accessible as symbol" do
+ test "config_for loads custom configuration from yaml accessible as symbol or string" do
app_file "config/custom.yml", <<-RUBY
development:
foo: 'bar'
@@ -1727,6 +1748,7 @@ module ApplicationTests
app "development"
assert_equal "bar", Rails.application.config.my_custom_config[:foo]
+ assert_equal "bar", Rails.application.config.my_custom_config["foo"]
end
test "config_for loads nested custom configuration from yaml as symbol keys" do
@@ -1746,6 +1768,25 @@ module ApplicationTests
assert_equal 1, Rails.application.config.my_custom_config[:foo][:bar][:baz]
end
+ test "config_for loads nested custom configuration from yaml with deprecated non-symbol access" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo:
+ bar:
+ baz: 1
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ app "development"
+
+ assert_deprecated do
+ assert_equal 1, Rails.application.config.my_custom_config["foo"]["bar"]["baz"]
+ end
+ end
+
test "config_for makes all hash methods available" do
app_file "config/custom.yml", <<-RUBY
development:
@@ -1762,12 +1803,93 @@ module ApplicationTests
actual = Rails.application.config.my_custom_config
- assert_equal actual, foo: 0, bar: { baz: 1 }
- assert_equal actual.keys, [ :foo, :bar ]
- assert_equal actual.values, [ 0, baz: 1]
- assert_equal actual.to_h, foo: 0, bar: { baz: 1 }
- assert_equal actual[:foo], 0
- assert_equal actual[:bar], baz: 1
+ assert_equal({ foo: 0, bar: { baz: 1 } }, actual)
+ assert_equal([ :foo, :bar ], actual.keys)
+ assert_equal([ 0, baz: 1], actual.values)
+ assert_equal({ foo: 0, bar: { baz: 1 } }, actual.to_h)
+ assert_equal(0, actual[:foo])
+ assert_equal({ baz: 1 }, actual[:bar])
+ end
+
+ test "config_for generates deprecation notice when nested hash methods are called with non-symbols" do
+ app_file "config/custom.yml", <<-RUBY
+ development:
+ foo:
+ bar: 1
+ baz: 2
+ qux:
+ boo: 3
+ RUBY
+
+ app "development"
+
+ actual = Rails.application.config_for("custom")[:foo]
+
+ # slice
+ assert_deprecated do
+ assert_equal({ bar: 1, baz: 2 }, actual.slice("bar", "baz"))
+ end
+
+ # except
+ assert_deprecated do
+ assert_equal({ qux: { boo: 3 } }, actual.except("bar", "baz"))
+ end
+
+ # dig
+ assert_deprecated do
+ assert_equal(3, actual.dig("qux", "boo"))
+ end
+
+ # fetch - hit
+ assert_deprecated do
+ assert_equal(1, actual.fetch("bar", 0))
+ end
+
+ # fetch - miss
+ assert_deprecated do
+ assert_equal(0, actual.fetch("does-not-exist", 0))
+ end
+
+ # fetch_values
+ assert_deprecated do
+ assert_equal([1, 2], actual.fetch_values("bar", "baz"))
+ end
+
+ # key? - hit
+ assert_deprecated do
+ assert(actual.key?("bar"))
+ end
+
+ # key? - miss
+ assert_deprecated do
+ assert_not(actual.key?("does-not-exist"))
+ end
+
+ # slice!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ slice = actual.slice!("bar", "baz")
+ assert_equal({ bar: 1, baz: 2 }, actual)
+ assert_equal({ qux: { boo: 3 } }, slice)
+ end
+
+ # extract!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ extracted = actual.extract!("bar", "baz")
+ assert_equal({ bar: 1, baz: 2 }, extracted)
+ assert_equal({ qux: { boo: 3 } }, actual)
+ end
+
+ # except!
+ actual = Rails.application.config_for("custom")[:foo]
+
+ assert_deprecated do
+ actual.except!("bar", "baz")
+ assert_equal({ qux: { boo: 3 } }, actual)
+ end
end
test "config_for uses the Pathname object if it is provided" do
@@ -2094,7 +2216,9 @@ module ApplicationTests
test "ActionView::Template.finalize_compiled_template_methods is true by default" do
app "test"
- assert_equal true, ActionView::Template.finalize_compiled_template_methods
+ assert_deprecated do
+ ActionView::Template.finalize_compiled_template_methods
+ end
end
test "ActionView::Template.finalize_compiled_template_methods can be configured via config.action_view.finalize_compiled_template_methods" do
@@ -2106,7 +2230,9 @@ module ApplicationTests
app "test"
- assert_equal false, ActionView::Template.finalize_compiled_template_methods
+ assert_deprecated do
+ ActionView::Template.finalize_compiled_template_methods
+ end
end
test "ActiveJob::Base.return_false_on_aborted_enqueue is true by default" do
@@ -2289,6 +2415,11 @@ module ApplicationTests
MESSAGE
end
+ test "hosts include .localhost in development" do
+ app "development"
+ assert_includes Rails.application.config.hosts, ".localhost"
+ end
+
private
def force_lazy_load_hooks
yield # Tasty clarifying sugar, homie! We only need to reference a constant to load it.
diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
index ba186bda44..fb84276b8a 100644
--- a/railties/test/application/mailer_previews_test.rb
+++ b/railties/test/application/mailer_previews_test.rb
@@ -85,6 +85,7 @@ module ApplicationTests
end
test "mailer previews are loaded from a custom preview_path" do
+ app_dir "lib/mailer_previews"
add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
mailer "notifier", <<-RUBY
@@ -254,6 +255,7 @@ module ApplicationTests
end
test "mailer previews are reloaded from a custom preview_path" do
+ app_dir "lib/mailer_previews"
add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
app("development")
@@ -818,6 +820,7 @@ module ApplicationTests
def build_app
super
app_file "config/routes.rb", "Rails.application.routes.draw do; end"
+ app_dir "test/mailers/previews"
end
def mailer(name, contents)
diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb
index 432344bccc..f0f1112f6b 100644
--- a/railties/test/application/multiple_applications_test.rb
+++ b/railties/test/application/multiple_applications_test.rb
@@ -100,30 +100,6 @@ module ApplicationTests
assert_nothing_raised { AppTemplate::Application.new }
end
- def test_initializers_run_on_different_applications_go_to_the_same_class
- application1 = AppTemplate::Application.new
- run_count = 0
-
- AppTemplate::Application.initializer :init0 do
- run_count += 1
- end
-
- application1.initializer :init1 do
- run_count += 1
- end
-
- AppTemplate::Application.new.initializer :init2 do
- run_count += 1
- end
-
- assert_equal 0, run_count, "Without loading the initializers, the count should be 0"
-
- # Set config.eager_load to false so that an eager_load warning doesn't pop up
- AppTemplate::Application.create { config.eager_load = false }.initialize!
-
- assert_equal 3, run_count, "There should have been three initializers that incremented the count"
- end
-
def test_consoles_run_on_different_applications_go_to_the_same_class
run_count = 0
AppTemplate::Application.console { run_count += 1 }
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index 84ac2f057e..5879949b61 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -93,6 +93,8 @@ module ApplicationTests
def with_bad_permissions
Dir.chdir(app_path) do
+ skip "Can't avoid permissions as root" if Process.uid.zero?
+
set_database_url
FileUtils.chmod("-w", "db")
yield
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index 44e3b0f66b..fe56e3d076 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -145,8 +145,8 @@ module ApplicationTests
# loading a specific fixture
rails "db:fixtures:load", "FIXTURES=products"
- assert_equal 2, ::AppTemplate::Application::Product.count
- assert_equal 0, ::AppTemplate::Application::User.count
+ assert_equal 2, Product.count
+ assert_equal 0, User.count
end
def test_loading_only_yml_fixtures
diff --git a/railties/test/application/server_test.rb b/railties/test/application/server_test.rb
index f4bd09903a..9df36b3444 100644
--- a/railties/test/application/server_test.rb
+++ b/railties/test/application/server_test.rb
@@ -29,8 +29,8 @@ module ApplicationTests
primary, replica = PTY.open
pid = nil
- begin
- pid = Process.spawn("#{app_path}/bin/rails server -P tmp/dummy.pid", in: replica, out: replica, err: replica)
+ Bundler.with_original_env do
+ pid = Process.spawn("bin/rails server -P tmp/dummy.pid", chdir: app_path, in: replica, out: replica, err: replica)
assert_output("Listening", primary)
rails("restart")
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index fda6df500d..0bdd2b314d 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -16,7 +16,7 @@ module ApplicationTests
teardown_app
end
- def test_run_via_backwardscompatibility
+ def test_run_via_backwards_compatibility
require "minitest/rails_plugin"
assert_nothing_raised do
@@ -742,6 +742,7 @@ module ApplicationTests
def test_reset_sessions_before_rollback_on_system_tests
app_file "test/system/reset_session_before_rollback_test.rb", <<-RUBY
require "application_system_test_case"
+ require "selenium/webdriver"
class ResetSessionBeforeRollbackTest < ApplicationSystemTestCase
def teardown_fixtures
@@ -770,6 +771,7 @@ module ApplicationTests
def test_reset_sessions_on_failed_system_test_screenshot
app_file "test/system/reset_sessions_on_failed_system_test_screenshot_test.rb", <<~RUBY
require "application_system_test_case"
+ require "selenium/webdriver"
class ResetSessionsOnFailedSystemTestScreenshotTest < ApplicationSystemTestCase
ActionDispatch::SystemTestCase.class_eval do
@@ -826,6 +828,7 @@ module ApplicationTests
def test_system_tests_are_run_through_rake_test_when_given_in_TEST
app_file "test/system/dummy_test.rb", <<-RUBY
require "application_system_test_case"
+ require "selenium/webdriver"
class DummyTest < ApplicationSystemTestCase
test "something" do
diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb
index f22b9fda3d..6bcc0b0acb 100644
--- a/railties/test/application/url_generation_test.rb
+++ b/railties/test/application/url_generation_test.rb
@@ -19,6 +19,7 @@ module ApplicationTests
config.session_store :cookie_store, key: "_myapp_session"
config.active_support.deprecation = :log
config.eager_load = false
+ config.hosts << proc { true }
end
Rails.application.initialize!
diff --git a/railties/test/application/zeitwerk_integration_test.rb b/railties/test/application/zeitwerk_integration_test.rb
new file mode 100644
index 0000000000..628a85acd8
--- /dev/null
+++ b/railties/test/application/zeitwerk_integration_test.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+require "isolation/abstract_unit"
+require "active_support/dependencies/zeitwerk_integration"
+
+class ZeitwerkIntegrationTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ end
+
+ def boot(env = "development")
+ app(env)
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def deps
+ ActiveSupport::Dependencies
+ end
+
+ def decorated?
+ deps.singleton_class < deps::ZeitwerkIntegration::Decorations
+ end
+
+ test "ActiveSupport::Dependencies is decorated by default" do
+ boot
+
+ assert decorated?
+ assert_instance_of Zeitwerk::Loader, Rails.autoloader
+ assert_instance_of Zeitwerk::Loader, Rails.once_autoloader
+ assert_equal [Rails.autoloader, Rails.once_autoloader], Rails.autoloaders
+ end
+
+ test "ActiveSupport::Dependencies is not decorated in classic mode" do
+ add_to_config "config.autoloader = :classic"
+ boot
+
+ assert_not decorated?
+ assert_nil Rails.autoloader
+ assert_nil Rails.once_autoloader
+ assert_empty Rails.autoloaders
+ end
+
+ test "constantize returns the value stored in the constant" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ boot
+
+ assert_same Admin::User, deps.constantize("Admin::User")
+ end
+
+ test "constantize raises if the constant is unknown" do
+ boot
+
+ assert_raises(NameError) { deps.constantize("Admin") }
+ end
+
+ test "safe_constantize returns the value stored in the constant" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ boot
+
+ assert_same Admin::User, deps.safe_constantize("Admin::User")
+ end
+
+ test "safe_constantize returns nil for unknown constants" do
+ boot
+
+ assert_nil deps.safe_constantize("Admin")
+ end
+
+ test "autoloaded_constants returns autoloaded constant paths" do
+ app_file "app/models/admin/user.rb", "class Admin::User; end"
+ app_file "app/models/post.rb", "class Post; end"
+ boot
+
+ assert Admin::User
+ assert_equal ["Admin", "Admin::User"], deps.autoloaded_constants
+ end
+
+ test "autoloaded? says if a constant has been autoloaded" do
+ app_file "app/models/user.rb", "class User; end"
+ app_file "app/models/post.rb", "class Post; end"
+ boot
+
+ assert Post
+ assert deps.autoloaded?("Post")
+ assert deps.autoloaded?(Post)
+ assert_not deps.autoloaded?("User")
+ end
+
+ test "eager loading loads the application code" do
+ $zeitwerk_integration_test_user = false
+ $zeitwerk_integration_test_post = false
+
+ app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
+ app_file "app/models/post.rb", "class Post; end; $zeitwerk_integration_test_post = true"
+ boot("production")
+
+ assert $zeitwerk_integration_test_user
+ assert $zeitwerk_integration_test_post
+ end
+
+ test "eager loading loads anything managed by Zeitwerk" do
+ $zeitwerk_integration_test_user = false
+ app_file "app/models/user.rb", "class User; end; $zeitwerk_integration_test_user = true"
+
+ $zeitwerk_integration_test_extras = false
+ app_dir "extras"
+ app_file "extras/webhook_hacks.rb", "WebhookHacks = 1; $zeitwerk_integration_test_extras = true"
+
+ require "zeitwerk"
+ autoloader = Zeitwerk::Loader.new
+ autoloader.push_dir("#{app_path}/extras")
+ autoloader.setup
+
+ boot("production")
+
+ assert $zeitwerk_integration_test_user
+ assert $zeitwerk_integration_test_extras
+ end
+
+ test "autoload paths that are below Gem.path go to the once autoloader" do
+ app_dir "extras"
+ add_to_config 'config.autoload_paths << "#{Rails.root}/extras"'
+
+ # Mocks Gem.path to include the extras directory.
+ Gem.singleton_class.prepend(
+ Module.new do
+ def path
+ super + ["#{Rails.root}/extras"]
+ end
+ end
+ )
+ boot
+
+ assert_not_includes Rails.autoloader.dirs, "#{app_path}/extras"
+ assert_includes Rails.once_autoloader.dirs, "#{app_path}/extras"
+ end
+
+ test "clear reloads the main autoloader, and does not reload the once one" do
+ boot
+
+ $zeitwerk_integration_reload_test = []
+
+ autoloader = Rails.autoloader
+ def autoloader.reload
+ $zeitwerk_integration_reload_test << :autoloader
+ super
+ end
+
+ once_autoloader = Rails.once_autoloader
+ def once_autoloader.reload
+ $zeitwerk_integration_reload_test << :once_autoloader
+ super
+ end
+
+ ActiveSupport::Dependencies.clear
+
+ assert_equal %i(autoloader), $zeitwerk_integration_reload_test
+ end
+end
diff --git a/railties/test/command/base_test.rb b/railties/test/command/base_test.rb
index a49ae8aae7..9132c8b4af 100644
--- a/railties/test/command/base_test.rb
+++ b/railties/test/command/base_test.rb
@@ -4,10 +4,12 @@ require "abstract_unit"
require "rails/command"
require "rails/commands/generate/generate_command"
require "rails/commands/secrets/secrets_command"
+require "rails/commands/db/system/change/change_command"
class Rails::Command::BaseTest < ActiveSupport::TestCase
test "printing commands" do
assert_equal %w(generate), Rails::Command::GenerateCommand.printing_commands
assert_equal %w(secrets:setup secrets:edit secrets:show), Rails::Command::SecretsCommand.printing_commands
+ assert_equal %w(db:system:change), Rails::Command::Db::System::ChangeCommand.printing_commands
end
end
diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb
index 7842b0db61..10d0ba1cae 100644
--- a/railties/test/commands/credentials_test.rb
+++ b/railties/test/commands/credentials_test.rb
@@ -63,6 +63,14 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase
end
end
+ test "edit command does not raise when an initializer tries to access non-existent credentials" do
+ app_file "config/initializers/raise_when_loaded.rb", <<-RUBY
+ Rails.application.credentials.missing_key!
+ RUBY
+
+ assert_match(/access_key_id: 123/, run_edit_command(environment: "qa"))
+ end
+
test "show credentials" do
assert_match(/access_key_id: 123/, run_show_command)
end
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index adb168f7a3..65f6916acb 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -112,9 +112,9 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
def test_mysql_full
- start(adapter: "mysql2", database: "db", host: "locahost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8")
+ start(adapter: "mysql2", database: "db", host: "localhost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8")
assert_not aborted
- assert_equal [%w[mysql mysql5], "--host=locahost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args
+ assert_equal [%w[mysql mysql5], "--host=localhost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args
end
def test_mysql_include_password
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 4932100ea2..44d4e92256 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -320,6 +320,12 @@ class ActionsTest < Rails::Generators::TestCase
assert_no_file "app/models/my_model.rb"
end
+ def test_generate_should_run_command_without_env
+ assert_called_with(generator, :run, ["rails generate model MyModel name:string", verbose: false]) do
+ action :generate, "model", "MyModel", "name:string"
+ end
+ end
+
def test_rake_should_run_rake_command_with_default_env
assert_called_with(generator, :run, ["rake log:clear RAILS_ENV=development", verbose: false]) do
with_rails_env nil do
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index e0cd7f90ce..937b8eb427 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -199,7 +199,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_new_application_use_json_serialzier
+ def test_new_application_use_json_serializer
run_generator
assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
@@ -526,7 +526,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcsqlite3-adapter"
else
- assert_gem "sqlite3"
+ assert_gem "sqlite3", "'~> 1.3', '>= 1.3.6'"
end
end
@@ -660,6 +660,15 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_gem "jbuilder"
end
+ def test_inclusion_of_zeitwerk
+ run_generator
+ if RUBY_ENGINE == "ruby"
+ assert_gem "zeitwerk"
+ else
+ assert_no_gem "zeitwerk"
+ end
+ end
+
def test_inclusion_of_a_debugger
run_generator
if defined?(JRUBY_VERSION) || RUBY_ENGINE == "rbx"
diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb
index 975a204af4..8b42cb83db 100644
--- a/railties/test/generators/generators_test_helper.rb
+++ b/railties/test/generators/generators_test_helper.rb
@@ -79,7 +79,11 @@ module GeneratorsTestHelper
end
def evaluate_template(file, locals = {})
- erb = ERB.new(File.read(file), nil, "-", "@output_buffer")
+ erb = if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
+ ERB.new(File.read(file), trim_mode: "-", eoutvar: "@output_buffer")
+ else
+ ERB.new(File.read(file), nil, "-", "@output_buffer")
+ end
context = Class.new do
locals.each do |local, value|
class_attribute local, default: value
diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb
index ddac6e1a1e..099e06c4d3 100644
--- a/railties/test/generators/mailer_generator_test.rb
+++ b/railties/test/generators/mailer_generator_test.rb
@@ -119,7 +119,7 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_match(/haml \[not found\]/, content)
end
- def test_mailer_with_namedspaced_mailer
+ def test_mailer_with_namespaced_mailer
run_generator ["Farm::Animal", "moos"]
assert_file "app/mailers/farm/animal_mailer.rb" do |mailer|
assert_match(/class Farm::AnimalMailer < ApplicationMailer/, mailer)
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index b06db6dd8a..134659f285 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -104,7 +104,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
ActiveRecord::Base.pluralize_table_names = true
end
- def test_migration_with_namespaces_in_model_name_without_plurization
+ def test_migration_with_namespaces_in_model_name_without_pluralization
ActiveRecord::Base.pluralize_table_names = false
run_generator ["Gallery::Image"]
assert_migration "db/migrate/create_gallery_image", /class CreateGalleryImage < ActiveRecord::Migration\[[0-9.]+\]/
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index 4ec656d18b..f45464f8d0 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -442,7 +442,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
end
- def test_dummy_appplication_skip_listen_by_default
+ def test_dummy_application_skip_listen_by_default
run_generator
assert_file "test/dummy/config/environments/development.rb" do |contents|
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 01b6cb0a43..47d42645c6 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -421,6 +421,10 @@ module TestHelpers
file_name
end
+ def app_dir(path)
+ FileUtils.mkdir_p("#{app_path}/#{path}")
+ end
+
def remove_file(path)
FileUtils.rm_rf "#{app_path}/#{path}"
end
@@ -472,26 +476,26 @@ Module.new do
FileUtils.rm_rf(app_template_path)
FileUtils.mkdir_p(app_template_path)
- Dir.chdir "#{RAILS_FRAMEWORK_ROOT}/actionview" do
- `yarn build`
- end
-
- `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-bundle --skip-listen --no-rc`
+ `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-bundle --skip-listen --no-rc --skip-webpack-install`
File.open("#{app_template_path}/config/boot.rb", "w") do |f|
f.puts "require 'rails/all'"
end
- Dir.chdir(app_template_path) { `yarn add https://github.com/rails/webpacker.git` } # Use the latest version.
-
- # Manually install `webpack` as bin symlinks are not created for subdependencies
- # in workspaces. See https://github.com/yarnpkg/yarn/issues/4964
- Dir.chdir(app_template_path) { `yarn add webpack@4.17.1 --tilde` }
- Dir.chdir(app_template_path) { `yarn add webpack-cli` }
+ assets_path = "#{RAILS_FRAMEWORK_ROOT}/railties/test/isolation/assets"
+ FileUtils.cp("#{assets_path}/package.json", "#{app_template_path}/package.json")
+ FileUtils.cp("#{assets_path}/config/webpacker.yml", "#{app_template_path}/config/webpacker.yml")
+ FileUtils.cp_r("#{assets_path}/config/webpack", "#{app_template_path}/config/webpack")
+ FileUtils.ln_s("#{assets_path}/node_modules", "#{app_template_path}/node_modules")
+ FileUtils.chdir(app_template_path) { `bin/rails webpacker:binstubs` }
# Fake 'Bundler.require' -- we run using the repo's Gemfile, not an
# app-specific one: we don't want to require every gem that lists.
contents = File.read("#{app_template_path}/config/application.rb")
- contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }")
+ if RUBY_ENGINE == "ruby"
+ contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker zeitwerk).each { |r| require r }")
+ else
+ contents.sub!(/^Bundler\.require.*/, "%w(turbolinks webpacker).each { |r| require r }")
+ end
File.write("#{app_template_path}/config/application.rb", contents)
require "rails"
@@ -504,6 +508,8 @@ Module.new do
require "action_view"
require "active_storage"
require "action_cable"
+ require "action_mailbox"
+ require "action_text"
require "sprockets"
require "action_view/helpers"
diff --git a/railties/test/isolation/assets/config/webpack/development.js b/railties/test/isolation/assets/config/webpack/development.js
new file mode 100644
index 0000000000..395290f431
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/development.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpack/production.js b/railties/test/isolation/assets/config/webpack/production.js
new file mode 100644
index 0000000000..d064a6a7fb
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/production.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'production'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpack/test.js b/railties/test/isolation/assets/config/webpack/test.js
new file mode 100644
index 0000000000..395290f431
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpack/test.js
@@ -0,0 +1,3 @@
+process.env.NODE_ENV = process.env.NODE_ENV || 'development'
+const { environment } = require('@rails/webpacker')
+module.exports = environment.toWebpackConfig()
diff --git a/railties/test/isolation/assets/config/webpacker.yml b/railties/test/isolation/assets/config/webpacker.yml
new file mode 100644
index 0000000000..0b1f43a407
--- /dev/null
+++ b/railties/test/isolation/assets/config/webpacker.yml
@@ -0,0 +1,8 @@
+default: &default
+ check_yarn_integrity: false
+development:
+ <<: *default
+test:
+ <<: *default
+production:
+ <<: *default
diff --git a/railties/test/isolation/assets/package.json b/railties/test/isolation/assets/package.json
new file mode 100644
index 0000000000..7c34450fe0
--- /dev/null
+++ b/railties/test/isolation/assets/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "dummy",
+ "private": true,
+ "dependencies": {
+ "@rails/actioncable": "file:../../../../actioncable",
+ "@rails/activestorage": "file:../../../../activestorage",
+ "@rails/ujs": "file:../../../../actionview",
+ "@rails/webpacker": "https://github.com/rails/webpacker.git",
+ "turbolinks": "^5.2.0"
+ }
+}
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index 508367212b..69f6e34d58 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -87,11 +87,10 @@ module RailtiesTest
end
RUBY
+ restrict_frameworks
boot_rails
Dir.chdir(app_path) do
- # Install Active Storage, Action Mailbox, and Action Text migration files first so as not to affect test.
- `bundle exec rake active_storage:install action_mailbox:install action_text:install`
output = `bundle exec rake bukkits:install:migrations`
["CreateUsers", "AddLastNameToUsers", "CreateSessions"].each do |migration_name|
@@ -174,11 +173,10 @@ module RailtiesTest
class CreateKeys < ActiveRecord::Migration::Current; end
RUBY
+ restrict_frameworks
boot_rails
Dir.chdir(app_path) do
- # Install Active Storage, Action Mailbox, and Action Text migrations first so as not to affect test.
- `bundle exec rake active_storage:install action_mailbox:install action_text:install`
output = `bundle exec rake railties:install:migrations`.split("\n")
assert_match(/Copied migration \d+_create_users\.core_engine\.rb from core_engine/, output.first)
@@ -706,25 +704,27 @@ YAML
RUBY
@plugin.write "app/controllers/bukkits/foo_controller.rb", <<-RUBY
- class Bukkits::FooController < ActionController::Base
- def index
- render inline: "<%= help_the_engine %>"
- end
+ module Bukkits
+ class FooController < ActionController::Base
+ def index
+ render inline: "<%= help_the_engine %>"
+ end
- def show
- render plain: foo_path
- end
+ def show
+ render plain: foo_path
+ end
- def from_app
- render inline: "<%= (self.respond_to?(:bar_path) || self.respond_to?(:something)) %>"
- end
+ def from_app
+ render inline: "<%= (self.respond_to?(:bar_path) || self.respond_to?(:something)) %>"
+ end
- def routes_helpers_in_view
- render inline: "<%= foo_path %>, <%= main_app.bar_path %>"
- end
+ def routes_helpers_in_view
+ render inline: "<%= foo_path %>, <%= main_app.bar_path %>"
+ end
- def polymorphic_path_without_namespace
- render plain: polymorphic_path(Post.new)
+ def polymorphic_path_without_namespace
+ render plain: polymorphic_path(Post.new)
+ end
end
end
RUBY
@@ -1509,5 +1509,25 @@ YAML
def app
Rails.application
end
+
+ # Restrict frameworks to load in order to avoid engine frameworks affect tests.
+ def restrict_frameworks
+ remove_from_config("require 'rails/all'")
+ remove_from_config("require_relative 'boot'")
+ remove_from_env_config("development", "config.active_storage.*")
+ frameworks = <<~RUBY
+ require "rails"
+ require "active_model/railtie"
+ require "active_job/railtie"
+ require "active_record/railtie"
+ require "action_controller/railtie"
+ require "action_mailer/railtie"
+ require "action_view/railtie"
+ require "sprockets/railtie"
+ require "rails/test_unit/railtie"
+ RUBY
+ environment = File.read("#{app_path}/config/application.rb")
+ File.open("#{app_path}/config/application.rb", "w") { |f| f.puts frameworks + "\n" + environment }
+ end
end
end
diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb
index 48f0fbc80f..9755823e51 100644
--- a/railties/test/railties/mounted_engine_test.rb
+++ b/railties/test/railties/mounted_engine_test.rb
@@ -232,7 +232,7 @@ module ApplicationTests
get "/someone/blog/generate_application_route"
assert_equal "/", last_response.body
- get "/somone/blog/application_route_in_view"
+ get "/someone/blog/application_route_in_view"
assert_equal "/", last_response.body
# test generating engine's route from other engine
@@ -258,8 +258,8 @@ module ApplicationTests
assert_equal "http://example.org/anonymous/blog/posts/44", last_response.body
# test that correct path is generated for the same polymorphic_path call in an engine
- get "/somone/blog/engine_polymorphic_path"
- assert_equal "/somone/blog/posts/44", last_response.body
+ get "/someone/blog/engine_polymorphic_path"
+ assert_equal "/someone/blog/posts/44", last_response.body
# and in an application
get "/application_polymorphic_path"
diff --git a/tasks/release.rb b/tasks/release.rb
index 6784330fd6..82d1fb6a68 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -218,7 +218,7 @@ namespace :all do
<p>
<% if @user.avatar.attached? -%>
- <%= image_tag @user.avatar.representation(resize_to_fit: [500, 500]) %>
+ <%= image_tag @user.avatar.representation(resize_to_limit: [500, 500]) %>
<% end -%>
</p>
CODE
@@ -314,8 +314,7 @@ task :announce do
require "erb"
template = File.read("../tasks/release_announcement_draft.erb")
- match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /)
- if match && match[:version] >= "2.2.0" # Ruby 2.6+
+ if ERB.instance_method(:initialize).parameters.assoc(:key) # Ruby 2.6+
puts ERB.new(template, trim_mode: "<>").result(binding)
else
puts ERB.new(template, nil, "<>").result(binding)