aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitattributes2
-rw-r--r--.github/issue_template.md15
-rw-r--r--.github/pull_request_template.md18
-rw-r--r--.travis.yml13
-rw-r--r--Gemfile14
-rw-r--r--Gemfile.lock95
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md4
-rw-r--r--Rakefile10
-rw-r--r--actioncable/CHANGELOG.md58
-rw-r--r--actioncable/README.md52
-rw-r--r--actioncable/Rakefile8
-rw-r--r--actioncable/app/assets/javascripts/action_cable.coffee.erb14
-rw-r--r--actioncable/app/assets/javascripts/action_cable/connection.coffee79
-rw-r--r--actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee78
-rw-r--r--actioncable/app/assets/javascripts/action_cable/consumer.coffee25
-rw-r--r--actioncable/app/assets/javascripts/action_cable/subscription.coffee28
-rw-r--r--actioncable/app/assets/javascripts/action_cable/subscriptions.coffee14
-rw-r--r--actioncable/lib/action_cable.rb9
-rw-r--r--actioncable/lib/action_cable/channel/base.rb43
-rw-r--r--actioncable/lib/action_cable/channel/periodic_timers.rb52
-rw-r--r--actioncable/lib/action_cable/channel/streams.rb56
-rw-r--r--actioncable/lib/action_cable/connection.rb2
-rw-r--r--actioncable/lib/action_cable/connection/base.rb69
-rw-r--r--actioncable/lib/action_cable/connection/client_socket.rb33
-rw-r--r--actioncable/lib/action_cable/connection/faye_client_socket.rb48
-rw-r--r--actioncable/lib/action_cable/connection/faye_event_loop.rb44
-rw-r--r--actioncable/lib/action_cable/connection/identification.rb2
-rw-r--r--actioncable/lib/action_cable/connection/internal_channel.rb8
-rw-r--r--actioncable/lib/action_cable/connection/message_buffer.rb9
-rw-r--r--actioncable/lib/action_cable/connection/stream.rb20
-rw-r--r--actioncable/lib/action_cable/connection/stream_event_loop.rb11
-rw-r--r--actioncable/lib/action_cable/connection/subscriptions.rb6
-rw-r--r--actioncable/lib/action_cable/connection/web_socket.rb8
-rw-r--r--actioncable/lib/action_cable/engine.rb44
-rw-r--r--actioncable/lib/action_cable/gem_version.rb2
-rw-r--r--actioncable/lib/action_cable/helpers/action_cable_helper.rb27
-rw-r--r--actioncable/lib/action_cable/remote_connections.rb4
-rw-r--r--actioncable/lib/action_cable/server/base.rb45
-rw-r--r--actioncable/lib/action_cable/server/broadcasting.rb29
-rw-r--r--actioncable/lib/action_cable/server/configuration.rb37
-rw-r--r--actioncable/lib/action_cable/server/connections.rb15
-rw-r--r--actioncable/lib/action_cable/server/worker.rb52
-rw-r--r--actioncable/lib/action_cable/server/worker/active_record_connection_management.rb5
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/async.rb11
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/evented_redis.rb6
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/postgresql.rb9
-rw-r--r--actioncable/lib/action_cable/subscription_adapter/redis.rb17
-rw-r--r--actioncable/lib/rails/generators/channel/USAGE2
-rw-r--r--actioncable/lib/rails/generators/channel/channel_generator.rb22
-rw-r--r--actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb5
-rw-r--r--actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb5
-rw-r--r--actioncable/lib/rails/generators/channel/templates/assets/cable.js13
-rw-r--r--actioncable/test/channel/base_test.rb79
-rw-r--r--actioncable/test/channel/periodic_timers_test.rb48
-rw-r--r--actioncable/test/channel/rejection_test.rb2
-rw-r--r--actioncable/test/channel/stream_test.rb180
-rw-r--r--actioncable/test/client/echo_channel.rb4
-rw-r--r--actioncable/test/client_test.rb122
-rw-r--r--actioncable/test/connection/authorization_test.rb2
-rw-r--r--actioncable/test/connection/base_test.rb27
-rw-r--r--actioncable/test/connection/client_socket_test.rb64
-rw-r--r--actioncable/test/connection/cross_site_forgery_test.rb2
-rw-r--r--actioncable/test/connection/identifier_test.rb8
-rw-r--r--actioncable/test/connection/multiple_identifiers_test.rb2
-rw-r--r--actioncable/test/connection/stream_test.rb66
-rw-r--r--actioncable/test/connection/string_identifier_test.rb2
-rw-r--r--actioncable/test/connection/subscriptions_test.rb4
-rw-r--r--actioncable/test/server/broadcasting_test.rb15
-rw-r--r--actioncable/test/stubs/test_connection.rb24
-rw-r--r--actioncable/test/stubs/test_server.rb30
-rw-r--r--actioncable/test/subscription_adapter/base_test.rb6
-rw-r--r--actioncable/test/subscription_adapter/common.rb26
-rw-r--r--actioncable/test/subscription_adapter/evented_redis_test.rb11
-rw-r--r--actioncable/test/subscription_adapter/postgresql_test.rb8
-rw-r--r--actioncable/test/test_helper.rb54
-rw-r--r--actioncable/test/worker_test.rb14
-rw-r--r--actionmailer/CHANGELOG.md41
-rw-r--r--actionmailer/lib/action_mailer/base.rb27
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb2
-rw-r--r--actionmailer/lib/action_mailer/gem_version.rb2
-rw-r--r--actionmailer/lib/action_mailer/inline_preview_interceptor.rb2
-rw-r--r--actionmailer/lib/action_mailer/message_delivery.rb16
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb25
-rw-r--r--actionmailer/lib/action_mailer/test_case.rb17
-rw-r--r--actionmailer/lib/rails/generators/mailer/mailer_generator.rb17
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb6
-rw-r--r--actionmailer/test/abstract_unit.rb1
-rw-r--r--actionmailer/test/caching_test.rb229
-rw-r--r--actionmailer/test/delivery_methods_test.rb2
-rw-r--r--actionmailer/test/fixtures/caching_mailer/_partial.html.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb3
-rw-r--r--actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb1
-rw-r--r--actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb3
-rw-r--r--actionmailer/test/i18n_with_controller_test.rb4
-rw-r--r--actionmailer/test/mailers/caching_mailer.rb15
-rw-r--r--actionmailer/test/message_delivery_test.rb8
-rw-r--r--actionmailer/test/url_test.rb14
-rw-r--r--actionpack/CHANGELOG.md130
-rw-r--r--actionpack/actionpack.gemspec2
-rw-r--r--actionpack/lib/abstract_controller.rb6
-rw-r--r--actionpack/lib/abstract_controller/base.rb4
-rw-r--r--actionpack/lib/abstract_controller/caching.rb62
-rw-r--r--actionpack/lib/abstract_controller/caching/fragments.rb (renamed from actionpack/lib/action_controller/caching/fragments.rb)11
-rw-r--r--actionpack/lib/abstract_controller/error.rb4
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb3
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb1
-rw-r--r--actionpack/lib/action_controller.rb10
-rw-r--r--actionpack/lib/action_controller/api.rb39
-rw-r--r--actionpack/lib/action_controller/caching.rb67
-rw-r--r--actionpack/lib/action_controller/metal.rb5
-rw-r--r--actionpack/lib/action_controller/metal/basic_implicit_render.rb2
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb73
-rw-r--r--actionpack/lib/action_controller/metal/cookies.rb2
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb7
-rw-r--r--actionpack/lib/action_controller/metal/implicit_render.rb72
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb1
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb2
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb2
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb5
-rw-r--r--actionpack/lib/action_controller/metal/rescue.rb8
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb77
-rw-r--r--actionpack/lib/action_controller/test_case.rb6
-rw-r--r--actionpack/lib/action_dispatch.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb64
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb12
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb5
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb21
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb5
-rw-r--r--actionpack/lib/action_dispatch/journey/backwards.rb5
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb7
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb1
-rw-r--r--actionpack/lib/action_dispatch/middleware/executor.rb19
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb5
-rw-r--r--actionpack/lib/action_dispatch/middleware/load_interlock.rb21
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb66
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb22
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing.rb8
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb9
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb6
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb17
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb62
-rw-r--r--actionpack/lib/action_dispatch/testing/test_response.rb2
-rw-r--r--actionpack/lib/action_pack/gem_version.rb2
-rw-r--r--actionpack/test/abstract_unit.rb10
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb20
-rw-r--r--actionpack/test/controller/api/with_cookies_test.rb21
-rw-r--r--actionpack/test/controller/base_test.rb15
-rw-r--r--actionpack/test/controller/caching_test.rb27
-rw-r--r--actionpack/test/controller/flash_test.rb4
-rw-r--r--actionpack/test/controller/force_ssl_test.rb9
-rw-r--r--actionpack/test/controller/integration_test.rb27
-rw-r--r--actionpack/test/controller/live_stream_test.rb2
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb6
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb108
-rw-r--r--actionpack/test/controller/new_base/content_type_test.rb4
-rw-r--r--actionpack/test/controller/new_base/render_body_test.rb4
-rw-r--r--actionpack/test/controller/new_base/render_html_test.rb4
-rw-r--r--actionpack/test/controller/new_base/render_plain_test.rb4
-rw-r--r--actionpack/test/controller/new_base/render_template_test.rb2
-rw-r--r--actionpack/test/controller/new_base/render_test.rb8
-rw-r--r--actionpack/test/controller/new_base/render_text_test.rb4
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb84
-rw-r--r--actionpack/test/controller/parameters/always_permitted_parameters_test.rb6
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb30
-rw-r--r--actionpack/test/controller/redirect_test.rb11
-rw-r--r--actionpack/test/controller/render_test.rb100
-rw-r--r--actionpack/test/controller/render_xml_test.rb5
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb124
-rw-r--r--actionpack/test/controller/rescue_test.rb33
-rw-r--r--actionpack/test/controller/routing_test.rb242
-rw-r--r--actionpack/test/controller/test_case_test.rb27
-rw-r--r--actionpack/test/controller/url_for_integration_test.rb15
-rw-r--r--actionpack/test/controller/url_for_test.rb15
-rw-r--r--actionpack/test/controller/url_rewriter_test.rb4
-rw-r--r--actionpack/test/controller/webservice_test.rb2
-rw-r--r--actionpack/test/dispatch/callbacks_test.rb14
-rw-r--r--actionpack/test/dispatch/executor_test.rb134
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb2
-rw-r--r--actionpack/test/dispatch/reloader_test.rb67
-rw-r--r--actionpack/test/dispatch/request/json_params_parsing_test.rb47
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb8
-rw-r--r--actionpack/test/dispatch/request/query_string_parsing_test.rb8
-rw-r--r--actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb4
-rw-r--r--actionpack/test/dispatch/request_test.rb35
-rw-r--r--actionpack/test/dispatch/response_test.rb35
-rw-r--r--actionpack/test/dispatch/routing/inspector_test.rb57
-rw-r--r--actionpack/test/dispatch/routing_test.rb80
-rw-r--r--actionpack/test/dispatch/session/cache_store_test.rb4
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb4
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb4
-rw-r--r--actionpack/test/dispatch/ssl_test.rb30
-rw-r--r--actionpack/test/fixtures/collection_cache/index.html.erb2
-rw-r--r--actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb2
-rw-r--r--actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb1
-rw-r--r--actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb (renamed from actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb)0
-rw-r--r--actionpack/test/journey/router_test.rb130
-rw-r--r--actionview/CHANGELOG.md67
-rw-r--r--actionview/actionview.gemspec8
-rw-r--r--actionview/lib/action_view/digestor.rb153
-rw-r--r--actionview/lib/action_view/flows.rb9
-rw-r--r--actionview/lib/action_view/gem_version.rb2
-rw-r--r--actionview/lib/action_view/helpers/atom_feed_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb45
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb6
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb10
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb9
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb2
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb1
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb65
-rw-r--r--actionview/lib/action_view/log_subscriber.rb36
-rw-r--r--actionview/lib/action_view/lookup_context.rb45
-rw-r--r--actionview/lib/action_view/railtie.rb2
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb10
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb26
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb47
-rw-r--r--actionview/lib/action_view/tasks/cache_digests.rake (renamed from actionview/lib/action_view/tasks/dependencies.rake)4
-rw-r--r--actionview/lib/action_view/template.rb23
-rw-r--r--actionview/lib/action_view/template/error.rb4
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb25
-rw-r--r--actionview/lib/action_view/template/resolver.rb16
-rw-r--r--actionview/lib/action_view/template/types.rb1
-rw-r--r--actionview/lib/action_view/view_paths.rb4
-rw-r--r--actionview/test/abstract_unit.rb14
-rw-r--r--actionview/test/fixtures/digestor/messages/peek.html.erb2
-rw-r--r--actionview/test/fixtures/test/_customer.mobile.erb1
-rw-r--r--actionview/test/fixtures/test/_🍣.erb1
-rw-r--r--actionview/test/lib/controller/fake_models.rb21
-rw-r--r--actionview/test/template/digestor_test.rb65
-rw-r--r--actionview/test/template/form_helper_test.rb56
-rw-r--r--actionview/test/template/form_tag_helper_test.rb4
-rw-r--r--actionview/test/template/log_subscriber_test.rb29
-rw-r--r--actionview/test/template/render_test.rb70
-rw-r--r--actionview/test/template/resolver_patterns_test.rb19
-rw-r--r--actionview/test/template/tag_helper_test.rb6
-rw-r--r--actionview/test/template/template_test.rb32
-rw-r--r--actionview/test/template/url_helper_test.rb50
-rw-r--r--activejob/CHANGELOG.md14
-rw-r--r--activejob/activejob.gemspec2
-rw-r--r--activejob/lib/active_job.rb1
-rw-r--r--activejob/lib/active_job/async_job.rb77
-rw-r--r--activejob/lib/active_job/callbacks.rb5
-rw-r--r--activejob/lib/active_job/core.rb4
-rw-r--r--activejob/lib/active_job/execution.rb6
-rw-r--r--activejob/lib/active_job/gem_version.rb2
-rw-r--r--activejob/lib/active_job/queue_adapters/async_adapter.rb105
-rw-r--r--activejob/lib/active_job/queue_adapters/inline_adapter.rb2
-rw-r--r--activejob/lib/active_job/railtie.rb9
-rw-r--r--activejob/lib/active_job/test_helper.rb569
-rw-r--r--activejob/lib/rails/generators/job/job_generator.rb15
-rw-r--r--activejob/lib/rails/generators/job/templates/application_job.rb4
-rw-r--r--activejob/test/adapters/async.rb4
-rw-r--r--activejob/test/cases/argument_serialization_test.rb13
-rw-r--r--activejob/test/cases/async_job_test.rb42
-rw-r--r--activejob/test/cases/job_serialization_test.rb23
-rw-r--r--activejob/test/helper.rb2
-rw-r--r--activejob/test/integration/queuing_test.rb4
-rw-r--r--activejob/test/support/integration/adapters/async.rb3
-rw-r--r--activejob/test/support/integration/dummy_app_template.rb2
-rw-r--r--activemodel/CHANGELOG.md10
-rw-r--r--activemodel/activemodel.gemspec2
-rw-r--r--activemodel/lib/active_model/dirty.rb14
-rw-r--r--activemodel/lib/active_model/errors.rb13
-rw-r--r--activemodel/lib/active_model/gem_version.rb2
-rw-r--r--activemodel/lib/active_model/type.rb2
-rw-r--r--activemodel/lib/active_model/type/decimal.rb12
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb7
-rw-r--r--activemodel/lib/active_model/validations/length.rb2
-rw-r--r--activemodel/lib/active_model/validator.rb4
-rw-r--r--activemodel/test/cases/errors_test.rb7
-rw-r--r--activemodel/test/cases/helper.rb2
-rw-r--r--activemodel/test/cases/type/decimal_test.rb7
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb2
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb26
-rw-r--r--activemodel/test/cases/validations_test.rb16
-rw-r--r--activerecord/CHANGELOG.md273
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc6
-rw-r--r--activerecord/activerecord.gemspec2
-rw-r--r--activerecord/examples/performance.rb1
-rw-r--r--activerecord/examples/simple.rb1
-rw-r--r--activerecord/lib/active_record.rb1
-rw-r--r--activerecord/lib/active_record/association_relation.rb2
-rw-r--r--activerecord/lib/active_record/associations.rb14
-rw-r--r--activerecord/lib/active_record/associations/association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb6
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb3
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb4
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb1
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb23
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb2
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb15
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb8
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb2
-rw-r--r--activerecord/lib/active_record/attributes.rb6
-rw-r--r--activerecord/lib/active_record/autosave_association.rb2
-rw-r--r--activerecord/lib/active_record/base.rb3
-rw-r--r--activerecord/lib/active_record/callbacks.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb30
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb54
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb173
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/quoting.rb51
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb1
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb64
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb48
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb47
-rw-r--r--activerecord/lib/active_record/core.rb17
-rw-r--r--activerecord/lib/active_record/enum.rb2
-rw-r--r--activerecord/lib/active_record/errors.rb9
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb8
-rw-r--r--activerecord/lib/active_record/gem_version.rb2
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/migration.rb8
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb4
-rw-r--r--activerecord/lib/active_record/model_schema.rb12
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb22
-rw-r--r--activerecord/lib/active_record/null_relation.rb2
-rw-r--r--activerecord/lib/active_record/persistence.rb2
-rw-r--r--activerecord/lib/active_record/query_cache.rb28
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/railtie.rb19
-rw-r--r--activerecord/lib/active_record/railties/databases.rake10
-rw-r--r--activerecord/lib/active_record/reflection.rb25
-rw-r--r--activerecord/lib/active_record/relation.rb52
-rw-r--r--activerecord/lib/active_record/relation/batches.rb40
-rw-r--r--activerecord/lib/active_record/relation/batches/batch_enumerator.rb2
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb3
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb74
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb13
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb10
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb57
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb6
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb2
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb3
-rw-r--r--activerecord/lib/active_record/sanitization.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb12
-rw-r--r--activerecord/lib/active_record/scoping/default.rb3
-rw-r--r--activerecord/lib/active_record/suppressor.rb6
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb14
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb2
-rw-r--r--activerecord/lib/active_record/type.rb2
-rw-r--r--activerecord/lib/active_record/type/internal/abstract_json.rb6
-rw-r--r--activerecord/lib/active_record/type/time.rb12
-rw-r--r--activerecord/lib/active_record/validations.rb2
-rw-r--r--activerecord/lib/active_record/validations/absence.rb1
-rw-r--r--activerecord/lib/active_record/validations/length.rb12
-rw-r--r--activerecord/lib/active_record/validations/presence.rb1
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb16
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb31
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/application_record.rb5
-rw-r--r--activerecord/test/cases/adapter_test.rb18
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb9
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb50
-rw-r--r--activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb45
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb32
-rw-r--r--activerecord/test/cases/adapters/mysql2/json_test.rb15
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb18
-rw-r--r--activerecord/test/cases/adapters/mysql2/quoting_test.rb21
-rw-r--r--activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb9
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb17
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb128
-rw-r--r--activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/serial_test.rb30
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb32
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb11
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb18
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_test.rb46
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb15
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb14
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb14
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb26
-rw-r--r--activerecord/test/cases/attributes_test.rb11
-rw-r--r--activerecord/test/cases/autosave_association_test.rb4
-rw-r--r--activerecord/test/cases/batches_test.rb40
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb3
-rw-r--r--activerecord/test/cases/calculations_test.rb4
-rw-r--r--activerecord/test/cases/comment_test.rb89
-rw-r--r--activerecord/test/cases/connection_management_test.rb64
-rw-r--r--activerecord/test/cases/counter_cache_test.rb2
-rw-r--r--activerecord/test/cases/database_statements_test.rb6
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb4
-rw-r--r--activerecord/test/cases/finder_test.rb117
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb6
-rw-r--r--activerecord/test/cases/helper.rb2
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb88
-rw-r--r--activerecord/test/cases/inheritance_test.rb5
-rw-r--r--activerecord/test/cases/locking_test.rb2
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb14
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb46
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb5
-rw-r--r--activerecord/test/cases/migration_test.rb38
-rw-r--r--activerecord/test/cases/modules_test.rb4
-rw-r--r--activerecord/test/cases/multiple_db_test.rb2
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb32
-rw-r--r--activerecord/test/cases/primary_keys_test.rb13
-rw-r--r--activerecord/test/cases/query_cache_test.rb56
-rw-r--r--activerecord/test/cases/relation/or_test.rb6
-rw-r--r--activerecord/test/cases/relation/where_test.rb14
-rw-r--r--activerecord/test/cases/relations_test.rb16
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb16
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb12
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb20
-rw-r--r--activerecord/test/cases/statement_cache_test.rb12
-rw-r--r--activerecord/test/cases/store_test.rb1
-rw-r--r--activerecord/test/cases/suppressor_test.rb13
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb42
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb41
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb34
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb31
-rw-r--r--activerecord/test/cases/time_precision_test.rb2
-rw-r--r--activerecord/test/cases/timestamp_test.rb11
-rw-r--r--activerecord/test/cases/validations/absence_validation_test.rb18
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb6
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb22
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb36
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb83
-rw-r--r--activerecord/test/cases/validations_test.rb2
-rw-r--r--activerecord/test/fixtures/price_estimates.yml11
-rw-r--r--activerecord/test/models/author.rb8
-rw-r--r--activerecord/test/models/car.rb2
-rw-r--r--activerecord/test/models/cat.rb10
-rw-r--r--activerecord/test/models/notification.rb1
-rw-r--r--activerecord/test/models/owner.rb3
-rw-r--r--activerecord/test/models/pet.rb3
-rw-r--r--activerecord/test/models/pet_treasure.rb6
-rw-r--r--activerecord/test/models/tag.rb6
-rw-r--r--activerecord/test/models/uuid_item.rb6
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb1
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb5
-rw-r--r--activerecord/test/schema/schema.rb21
-rw-r--r--activesupport/CHANGELOG.md124
-rw-r--r--activesupport/activesupport.gemspec2
-rw-r--r--activesupport/lib/active_support.rb3
-rw-r--r--activesupport/lib/active_support/benchmarkable.rb2
-rw-r--r--activesupport/lib/active_support/cache.rb88
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb16
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb2
-rw-r--r--activesupport/lib/active_support/callbacks.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/date_time.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/zones.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/marshal.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/module/introspection.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/conversions.rb98
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/time.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/object/json.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb2
-rw-r--r--activesupport/lib/active_support/dependencies.rb67
-rw-r--r--activesupport/lib/active_support/dependencies/interlock.rb14
-rw-r--r--activesupport/lib/active_support/deprecation/reporting.rb17
-rw-r--r--activesupport/lib/active_support/duration.rb24
-rw-r--r--activesupport/lib/active_support/duration/iso8601_parser.rb122
-rw-r--r--activesupport/lib/active_support/duration/iso8601_serializer.rb51
-rw-r--r--activesupport/lib/active_support/evented_file_update_checker.rb9
-rw-r--r--activesupport/lib/active_support/execution_wrapper.rb117
-rw-r--r--activesupport/lib/active_support/executor.rb6
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb24
-rw-r--r--activesupport/lib/active_support/gem_version.rb2
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb4
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb9
-rw-r--r--activesupport/lib/active_support/logger.rb7
-rw-r--r--activesupport/lib/active_support/logger_silence.rb25
-rw-r--r--activesupport/lib/active_support/logger_thread_safe_level.rb31
-rw-r--r--activesupport/lib/active_support/message_verifier.rb6
-rw-r--r--activesupport/lib/active_support/number_helper.rb127
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb2
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_phone_converter.rb13
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb8
-rw-r--r--activesupport/lib/active_support/reloader.rb129
-rw-r--r--activesupport/lib/active_support/rescuable.rb10
-rw-r--r--activesupport/lib/active_support/test_case.rb12
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb1
-rw-r--r--activesupport/test/abstract_unit.rb7
-rw-r--r--activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/throws.rb4
-rw-r--r--activesupport/test/benchmarkable_test.rb14
-rw-r--r--activesupport/test/caching_test.rb27
-rw-r--r--activesupport/test/core_ext/date_and_time_behavior.rb10
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb22
-rw-r--r--activesupport/test/core_ext/duration_test.rb90
-rw-r--r--activesupport/test/core_ext/hash/transform_keys_test.rb16
-rw-r--r--activesupport/test/core_ext/marshal_test.rb34
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb6
-rw-r--r--activesupport/test/core_ext/module_test.rb8
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb13
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb2
-rw-r--r--activesupport/test/dependencies_test.rb35
-rw-r--r--activesupport/test/executor_test.rb183
-rw-r--r--activesupport/test/file_update_checker_shared_tests.rb22
-rw-r--r--activesupport/test/json/encoding_test.rb5
-rw-r--r--activesupport/test/logger_test.rb44
-rw-r--r--activesupport/test/number_helper_test.rb2
-rw-r--r--activesupport/test/reloader_test.rb85
-rw-r--r--guides/CHANGELOG.md28
-rw-r--r--guides/assets/images/favicon.icobin5430 -> 16958 bytes
-rw-r--r--guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css2
-rw-r--r--guides/bug_report_templates/action_controller_master.rb5
-rw-r--r--guides/bug_report_templates/active_record_master.rb5
-rw-r--r--guides/bug_report_templates/generic_master.rb5
-rw-r--r--guides/source/2_2_release_notes.md2
-rw-r--r--guides/source/3_1_release_notes.md2
-rw-r--r--guides/source/4_0_release_notes.md2
-rw-r--r--guides/source/4_2_release_notes.md2
-rw-r--r--guides/source/5_0_release_notes.md109
-rw-r--r--guides/source/action_cable_overview.md621
-rw-r--r--guides/source/action_controller_overview.md4
-rw-r--r--guides/source/action_mailer_basics.md32
-rw-r--r--guides/source/action_view_overview.md20
-rw-r--r--guides/source/active_model_basics.md4
-rw-r--r--guides/source/active_record_basics.md2
-rw-r--r--guides/source/active_record_migrations.md19
-rw-r--r--guides/source/active_record_querying.md48
-rw-r--r--guides/source/active_record_validations.md35
-rw-r--r--guides/source/active_support_core_extensions.md31
-rw-r--r--guides/source/active_support_instrumentation.md4
-rw-r--r--guides/source/api_app.md44
-rw-r--r--guides/source/asset_pipeline.md19
-rw-r--r--guides/source/association_basics.md60
-rw-r--r--guides/source/caching_with_rails.md55
-rw-r--r--guides/source/command_line.md9
-rw-r--r--guides/source/configuring.md63
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md2
-rw-r--r--guides/source/debugging_rails_applications.md17
-rw-r--r--guides/source/development_dependencies_install.md1
-rw-r--r--guides/source/documents.yaml2
-rw-r--r--guides/source/engines.md12
-rw-r--r--guides/source/form_helpers.md4
-rw-r--r--guides/source/getting_started.md45
-rw-r--r--guides/source/i18n.md10
-rw-r--r--guides/source/initialization.md6
-rw-r--r--guides/source/layout.html.erb12
-rw-r--r--guides/source/layouts_and_rendering.md23
-rw-r--r--guides/source/rails_application_templates.md26
-rw-r--r--guides/source/rails_on_rack.md2
-rw-r--r--guides/source/routing.md4
-rw-r--r--guides/source/security.md10
-rw-r--r--guides/source/testing.md17
-rw-r--r--guides/source/upgrading_ruby_on_rails.md17
-rw-r--r--guides/source/working_with_javascript_in_rails.md12
-rw-r--r--load_paths.rb3
-rw-r--r--rails.gemspec2
-rw-r--r--railties/CHANGELOG.md79
-rw-r--r--railties/lib/rails/app_loader.rb2
-rw-r--r--railties/lib/rails/application.rb16
-rw-r--r--railties/lib/rails/application/bootstrap.rb7
-rw-r--r--railties/lib/rails/application/configuration.rb10
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb22
-rw-r--r--railties/lib/rails/application/finisher.rb94
-rw-r--r--railties/lib/rails/code_statistics.rb1
-rw-r--r--railties/lib/rails/command.rb70
-rw-r--r--railties/lib/rails/commands.rb4
-rw-r--r--railties/lib/rails/commands/runner.rb7
-rw-r--r--railties/lib/rails/commands/server.rb28
-rw-r--r--railties/lib/rails/console/app.rb3
-rw-r--r--railties/lib/rails/dev_caching.rb43
-rw-r--r--railties/lib/rails/engine/configuration.rb1
-rw-r--r--railties/lib/rails/gem_version.rb2
-rw-r--r--railties/lib/rails/generators.rb6
-rw-r--r--railties/lib/rails/generators/actions.rb28
-rw-r--r--railties/lib/rails/generators/actions/create_migration.rb1
-rw-r--r--railties/lib/rails/generators/app_base.rb21
-rw-r--r--railties/lib/rails/generators/erb/mailer/mailer_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb26
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/README.md2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Rakefile2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rails2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/setup2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/update2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config.ru5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config.ru.tt10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environment.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt15
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt22
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/puma.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/routes.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/spring.rb6
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/application.rb8
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb4
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb2
-rw-r--r--railties/lib/rails/railtie.rb86
-rw-r--r--railties/lib/rails/tasks/dev.rake12
-rw-r--r--railties/lib/rails/tasks/framework.rake25
-rw-r--r--railties/lib/rails/tasks/restart.rake9
-rw-r--r--railties/lib/rails/tasks/tmp.rake10
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb19
-rw-r--r--railties/lib/rails/test_unit/reporter.rb20
-rw-r--r--railties/railties.gemspec2
-rw-r--r--railties/test/abstract_unit.rb2
-rw-r--r--railties/test/application/assets_test.rb23
-rw-r--r--railties/test/application/bin_setup_test.rb2
-rw-r--r--railties/test/application/configuration_test.rb19
-rw-r--r--railties/test/application/console_test.rb11
-rw-r--r--railties/test/application/generators_test.rb10
-rw-r--r--railties/test/application/initializers/frameworks_test.rb2
-rw-r--r--railties/test/application/initializers/i18n_test.rb8
-rw-r--r--railties/test/application/integration_test_case_test.rb46
-rw-r--r--railties/test/application/loading_test.rb8
-rw-r--r--railties/test/application/middleware/exceptions_test.rb4
-rw-r--r--railties/test/application/middleware/remote_ip_test.rb6
-rw-r--r--railties/test/application/middleware/session_test.rb28
-rw-r--r--railties/test/application/middleware_test.rb26
-rw-r--r--railties/test/application/per_request_digest_cache_test.rb7
-rw-r--r--railties/test/application/rake/dbs_test.rb4
-rw-r--r--railties/test/application/rake/dev_test.rb11
-rw-r--r--railties/test/application/rake/restart_test.rb9
-rw-r--r--railties/test/application/rake_test.rb30
-rw-r--r--railties/test/application/test_runner_test.rb28
-rw-r--r--railties/test/commands/server_test.rb17
-rw-r--r--railties/test/generators/actions_test.rb48
-rw-r--r--railties/test/generators/api_app_generator_test.rb4
-rw-r--r--railties/test/generators/app_generator_test.rb144
-rw-r--r--railties/test/generators/channel_generator_test.rb32
-rw-r--r--railties/test/generators/job_generator_test.rb7
-rw-r--r--railties/test/generators/mailer_generator_test.rb6
-rw-r--r--railties/test/generators/model_generator_test.rb8
-rw-r--r--railties/test/generators/plugin_generator_test.rb54
-rw-r--r--railties/test/generators/plugin_test_runner_test.rb2
-rw-r--r--railties/test/generators/test_runner_in_engine_test.rb2
-rw-r--r--railties/test/isolation/abstract_unit.rb15
-rw-r--r--railties/test/railties/generators_test.rb6
-rw-r--r--railties/test/test_unit/reporter_test.rb21
-rw-r--r--tools/README.md3
-rwxr-xr-xtools/console4
-rw-r--r--tools/test.rb5
-rw-r--r--version.rb2
678 files changed, 11131 insertions, 4462 deletions
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..f6276855ce
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,2 @@
+*.rb diff=ruby
+*.gemspec diff=ruby
diff --git a/.github/issue_template.md b/.github/issue_template.md
new file mode 100644
index 0000000000..2d071d4a71
--- /dev/null
+++ b/.github/issue_template.md
@@ -0,0 +1,15 @@
+### Steps to reproduce
+
+(Guidelines for creating a bug report are [available
+here](http://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report))
+
+### Expected behavior
+Tell us what should happen
+
+### Actual behavior
+Tell us what happens instead
+
+### System configuration
+**Rails version**:
+
+**Ruby version**:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..48f7b0e214
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,18 @@
+### Summary
+
+Provide a general description of the code changes in your pull
+request... were there any bugs you had fixed? If so, mention them. If
+these bugs have open GitHub issues, be sure to tag them here as well,
+to keep the conversation linked together.
+
+### Other Information
+
+If there's anything else that's important and relevant to your pull
+request, mention that information here. This could include
+benchmarks, or other information.
+
+Finally, if your pull request affects documentation or any non-code
+changes, guidelines for those changes are [available
+here](http://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation)
+
+Thanks for contributing to Rails!
diff --git a/.travis.yml b/.travis.yml
index ae38617b99..f3314f49af 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,6 +17,7 @@ env:
- "GEM=railties"
- "GEM=ap"
- "GEM=ac"
+ - "GEM=ac FAYE=1"
- "GEM=am,amo,as,av,aj"
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
@@ -28,6 +29,18 @@ rvm:
- 2.3.0
- ruby-head
matrix:
+ include:
+ # Latest compiled version in http://rubies.travis-ci.org
+ - rvm: 2.3.0
+ env:
+ - "GEM=ar:mysql2"
+ addons:
+ mariadb: 10.0
+ - rvm: jruby-9.0.5.0
+ jdk: oraclejdk8
+ env:
+ - "JRUBY_OPTS='--dev -J-Xmx1024M'"
+ - "GEM='ap'"
allow_failures:
- rvm: ruby-head
fast_finish: true
diff --git a/Gemfile b/Gemfile
index f8779a3e49..5205896846 100644
--- a/Gemfile
+++ b/Gemfile
@@ -3,9 +3,9 @@ source 'https://rubygems.org'
gemspec
# We need a newish Rake since Active Job sets its test tasks' descriptions.
-gem 'rake', '>= 10.3'
+gem 'rake', '>= 11.1'
-# This needs to be with require false to ensure correct loading order, as has to
+# This needs to be with require false to ensure correct loading order, as it has to
# be loaded after loading the test library.
gem 'mocha', '~> 0.14', require: false
@@ -17,13 +17,7 @@ gem 'turbolinks', github: 'turbolinks/turbolinks-rails'
# require: false so bcrypt is loaded only when has_secure_password is used.
# This is to avoid Active Model (and by extension the entire framework)
# being dependent on a binary library.
-platforms :mingw, :x64_mingw, :mswin, :mswin64 do
- gem 'bcrypt-ruby', '~> 3.0.0', require: false
-end
-
-platforms :ruby, :jruby, :rbx do
- gem 'bcrypt', '~> 3.1.10', require: false
-end
+gem 'bcrypt', '~> 3.1.11', require: false
# This needs to be with require false to avoid it being automatically loaded by
# sprockets.
@@ -45,7 +39,7 @@ gem 'listen', '~> 3.0.5', require: false
# Active Job.
group :job do
- gem 'resque', require: false
+ gem 'resque', '< 1.26', require: false
gem 'resque-scheduler', require: false
gem 'sidekiq', require: false
gem 'sucker_punch', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index 0d2ce7b97f..ce854e6183 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -28,66 +28,66 @@ GIT
GIT
remote: git://github.com/turbolinks/turbolinks-rails.git
- revision: 1604dcd7bad911f1471d65e4f47cd19d844354f1
+ revision: 65884729016dbb4d032f12bb01b7e7c1ddeb68ac
specs:
- turbolinks (5.0.0.beta1)
+ turbolinks (5.0.0.beta2)
turbolinks-source
PATH
remote: .
specs:
- actioncable (5.0.0.beta2)
- actionpack (= 5.0.0.beta2)
+ actioncable (5.0.0.beta3)
+ actionpack (= 5.0.0.beta3)
nio4r (~> 1.2)
websocket-driver (~> 0.6.1)
- actionmailer (5.0.0.beta2)
- actionpack (= 5.0.0.beta2)
- actionview (= 5.0.0.beta2)
- activejob (= 5.0.0.beta2)
+ actionmailer (5.0.0.beta3)
+ actionpack (= 5.0.0.beta3)
+ actionview (= 5.0.0.beta3)
+ activejob (= 5.0.0.beta3)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
- actionpack (5.0.0.beta2)
- actionview (= 5.0.0.beta2)
- activesupport (= 5.0.0.beta2)
+ actionpack (5.0.0.beta3)
+ actionview (= 5.0.0.beta3)
+ activesupport (= 5.0.0.beta3)
rack (~> 2.x)
rack-test (~> 0.6.3)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- actionview (5.0.0.beta2)
- activesupport (= 5.0.0.beta2)
+ actionview (5.0.0.beta3)
+ activesupport (= 5.0.0.beta3)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
- activejob (5.0.0.beta2)
- activesupport (= 5.0.0.beta2)
+ activejob (5.0.0.beta3)
+ activesupport (= 5.0.0.beta3)
globalid (>= 0.3.6)
- activemodel (5.0.0.beta2)
- activesupport (= 5.0.0.beta2)
- activerecord (5.0.0.beta2)
- activemodel (= 5.0.0.beta2)
- activesupport (= 5.0.0.beta2)
+ activemodel (5.0.0.beta3)
+ activesupport (= 5.0.0.beta3)
+ activerecord (5.0.0.beta3)
+ activemodel (= 5.0.0.beta3)
+ activesupport (= 5.0.0.beta3)
arel (~> 7.0)
- activesupport (5.0.0.beta2)
+ activesupport (5.0.0.beta3)
concurrent-ruby (~> 1.0)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
- rails (5.0.0.beta2)
- actioncable (= 5.0.0.beta2)
- actionmailer (= 5.0.0.beta2)
- actionpack (= 5.0.0.beta2)
- actionview (= 5.0.0.beta2)
- activejob (= 5.0.0.beta2)
- activemodel (= 5.0.0.beta2)
- activerecord (= 5.0.0.beta2)
- activesupport (= 5.0.0.beta2)
+ rails (5.0.0.beta3)
+ actioncable (= 5.0.0.beta3)
+ actionmailer (= 5.0.0.beta3)
+ actionpack (= 5.0.0.beta3)
+ actionview (= 5.0.0.beta3)
+ activejob (= 5.0.0.beta3)
+ activemodel (= 5.0.0.beta3)
+ activerecord (= 5.0.0.beta3)
+ activesupport (= 5.0.0.beta3)
bundler (>= 1.3.0, < 2.0)
- railties (= 5.0.0.beta2)
+ railties (= 5.0.0.beta3)
sprockets-rails (>= 2.0.0)
- railties (5.0.0.beta2)
- actionpack (= 5.0.0.beta2)
- activesupport (= 5.0.0.beta2)
+ railties (5.0.0.beta3)
+ actionpack (= 5.0.0.beta3)
+ activesupport (= 5.0.0.beta3)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
@@ -100,9 +100,9 @@ GEM
backburner (1.2.0)
beaneater (~> 1.0)
dante (> 0.1.5)
- bcrypt (3.1.10)
- bcrypt-ruby (3.0.1)
- bcrypt-ruby (3.0.1-x86-mingw32)
+ bcrypt (3.1.11)
+ bcrypt (3.1.11-x64-mingw32)
+ bcrypt (3.1.11-x86-mingw32)
beaneater (1.0.0)
benchmark-ips (2.3.0)
builder (3.2.2)
@@ -116,9 +116,9 @@ GEM
coffee-script-source
execjs
coffee-script-source (1.10.0)
- concurrent-ruby (1.0.0)
+ concurrent-ruby (1.0.1)
connection_pool (2.2.0)
- dalli (2.7.5)
+ dalli (2.7.6)
dante (0.2.0)
delayed_job (4.1.1)
activesupport (>= 3.0, < 5.0)
@@ -131,7 +131,7 @@ GEM
erubis (2.7.0)
eventmachine (1.0.9.1)
execjs (2.6.0)
- faye-websocket (0.10.2)
+ faye-websocket (0.10.3)
eventmachine (>= 0.12.0)
websocket-driver (>= 0.5.1)
ffi (1.9.10)
@@ -158,7 +158,7 @@ GEM
mime-types (>= 1.16, < 3)
metaclass (0.0.4)
method_source (0.8.2)
- mime-types (2.99)
+ mime-types (2.99.1)
mini_portile2 (2.0.0)
minitest (5.3.3)
mocha (0.14.0)
@@ -197,7 +197,7 @@ GEM
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
- rake (10.5.0)
+ rake (11.1.1)
rb-fsevent (0.9.7)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
@@ -239,7 +239,7 @@ GEM
sprockets (3.5.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-rails (3.0.1)
+ sprockets-rails (3.0.4)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
@@ -252,7 +252,7 @@ GEM
thor (0.19.1)
thread (0.1.7)
thread_safe (0.3.5)
- turbolinks-source (5.0.0.beta1.1)
+ turbolinks-source (5.0.0.beta2)
tzinfo (1.2.2)
thread_safe (~> 0.1)
tzinfo-data (1.2015.7)
@@ -279,8 +279,7 @@ DEPENDENCIES
activerecord-jdbcpostgresql-adapter (>= 1.3.0)
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
backburner
- bcrypt (~> 3.1.10)
- bcrypt-ruby (~> 3.0.0)
+ bcrypt (~> 3.1.11)
benchmark-ips
byebug
coffee-rails (~> 4.1.0)
@@ -307,10 +306,10 @@ DEPENDENCIES
racc (>= 1.4.6)
rack-cache (~> 1.2)
rails!
- rake (>= 10.3)
+ rake (>= 11.1)
redcarpet (~> 3.2.3)
redis
- resque
+ resque (< 1.26)
resque-scheduler
sass!
sdoc (~> 0.4.0)
diff --git a/RAILS_VERSION b/RAILS_VERSION
index 60b8d0bf66..d727b28ee9 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-5.0.0.beta2
+5.0.0.beta3
diff --git a/README.md b/README.md
index 0c58685573..a2b726ea6c 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-## Welcome to Rails
+# Welcome to Rails
Rails is a web-application framework that includes everything needed to
create database-backed web applications according to the
@@ -35,7 +35,7 @@ or to generate the body of an email. In Rails, View generation is handled by Act
You can read more about Action View in its [README](actionview/README.rdoc).
Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails.
-In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library
+In addition to that, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library
to generate and send emails; Active Job ([README](activejob/README.md)), a
framework for declaring jobs and making them run on a variety of queueing
backends; Action Cable ([README](actioncable/README.md)), a framework to
diff --git a/Rakefile b/Rakefile
index b4517e2545..1d70ce96ea 100644
--- a/Rakefile
+++ b/Rakefile
@@ -21,7 +21,10 @@ task :default => %w(test test:isolated)
task task_name do
errors = []
FRAMEWORKS.each do |project|
- system(%(cd #{project} && #{$0} #{task_name})) || errors << project
+ system(%(cd #{project} && #{$0} #{task_name} --trace)) || errors << project
+ end
+ if task_name =~ /test/
+ system(%(cd actioncable && env FAYE=1 #{$0} #{task_name} --trace)) || errors << 'actioncable-faye'
end
fail("Errors in #{errors.join(', ')}") unless errors.empty?
end
@@ -30,9 +33,10 @@ end
desc "Smoke-test all projects"
task :smoke do
(FRAMEWORKS - %w(activerecord)).each do |project|
- system %(cd #{project} && #{$0} test:isolated)
+ system %(cd #{project} && #{$0} test:isolated --trace)
end
- system %(cd activerecord && #{$0} sqlite3:isolated_test)
+ system %(cd activerecord && #{$0} sqlite3:isolated_test --trace)
+ system %(cd actioncable && env FAYE=1 #{$0} test:isolated --trace)
end
desc "Install gems for all projects."
diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md
index bfc229d795..5162a31cf8 100644
--- a/actioncable/CHANGELOG.md
+++ b/actioncable/CHANGELOG.md
@@ -1,9 +1,55 @@
-* Added ActionCable::SubscriptionAdapter::EventedRedis.em_redis_connector/redis_connector and
- ActionCable::SubscriptionAdapter::Redis.redis_connector factory methods for redis connections,
- so you can overwrite with your own initializers. This is used when you want to use different-than-standard Redis adapters,
- like for Makara distributed Redis.
+* WebSocket protocol negotiation.
- *DHH*
+ Introduces an Action Cable protocol version that moves independently
+ of and, hopefully, more slowly than Action Cable itself. Client sockets
+ negotiate a protocol with the Cable server using WebSockets' native
+ subprotocol support:
+ * https://tools.ietf.org/html/rfc6455#section-1.9
+ * https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Subprotocols
+
+ If they can't negotiate a compatible protocol (usually due to upgrading
+ the Cable server with a browser still running old JavaScript) then the
+ client knows to disconnect, cease retrying, and tell the app that it hit
+ a protocol mismatch.
+
+ This allows us to evolve the Action Cable message format, handshaking,
+ pings, acknowledgements, and more without breaking older clients'
+ expectations of server behavior.
+
+ *Daniel Rhodes*
+
+* Pubsub: automatic stream decoding.
+
+ stream_for @room, coder: ActiveSupport::JSON do |message|
+ # `message` is a Ruby hash here instead of a JSON string
+
+ The `coder` must respond to `#decode`. Defaults to `coder: nil`
+ which skips decoding entirely.
+
+ *Jeremy Daer*
+
+* Add ActiveSupport::Notifications to ActionCable::Channel.
+
+ *Matthew Wear*
+
+* Safely support autoloading and class unloading, by preventing concurrent
+ loads, and disconnecting all cables during reload.
+
+ *Matthew Draper*
+
+* Ensure ActionCable behaves correctly for non-string queue names.
+
+ *Jay Hayes*
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* Added `em_redis_connector` and `redis_connector` to
+ `ActionCable::SubscriptionAdapter::EventedRedis` and added `redis_connector`
+ to `ActionCable::SubscriptionAdapter::Redis`, so you can overwrite with your
+ own initializers. This is used when you want to use different-than-standard
+ Redis adapters, like for Makara distributed Redis.
+
+ *DHH*
## Rails 5.0.0.beta2 (February 01, 2016) ##
@@ -26,7 +72,7 @@
ActionCable was changed from`config/redis/cable.yml` to
`config/cable.yml`.
- *Jon Moss*
+ *Jon Moss*
## Rails 5.0.0.beta1 (December 18, 2015) ##
diff --git a/actioncable/README.md b/actioncable/README.md
index c85d59a1c8..fe4d213485 100644
--- a/actioncable/README.md
+++ b/actioncable/README.md
@@ -39,7 +39,7 @@ reflections of each unit.
### A full-stack example
The first thing you must do is define your `ApplicationCable::Connection` class in Ruby. This
-is the place where you authorize the incoming connection, and proceed to establish it
+is the place where you authorize the incoming connection, and proceed to establish it,
if all is well. Here's the simplest example starting with the server-side connection class:
```ruby
@@ -73,7 +73,7 @@ use that to set the `current_user`. By identifying the connection by this same c
you're also ensuring that you can later retrieve all open connections by a given user (and
potentially disconnect them all if the user is deleted or deauthorized).
-Then you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put
+Next, you should define your `ApplicationCable::Channel` class in Ruby. This is the place where you put
shared logic between your channels.
```ruby
@@ -94,7 +94,7 @@ The client-side needs to setup a consumer instance of this connection. That's do
App.cable = ActionCable.createConsumer("ws://cable.example.com")
```
-The ws://cable.example.com address must point to your set of Action Cable servers, and it
+The `ws://cable.example.com` address must point to your Action Cable server(s), and it
must share a cookie namespace with the rest of the application (which may live under http://example.com).
This ensures that the signed cookie will be correctly sent.
@@ -105,8 +105,8 @@ is defined by declaring channels on the server and allowing the consumer to subs
### Channel example 1: User appearances
-Here's a simple example of a channel that tracks whether a user is online or not and what page they're on.
-(This is useful for creating presence features like showing a green dot next to a user name if they're online).
+Here's a simple example of a channel that tracks whether a user is online or not, and also what page they are currently on.
+(This is useful for creating presence features like showing a green dot next to a user's name if they're online).
First you declare the server-side channel:
@@ -178,9 +178,9 @@ App.cable.subscriptions.create "AppearanceChannel",
```
Simply calling `App.cable.subscriptions.create` will setup the subscription, which will call `AppearanceChannel#subscribed`,
-which in turn is linked to original `App.cable` -> `ApplicationCable::Connection` instances.
+which in turn is linked to the original `App.cable` -> `ApplicationCable::Connection` instances.
-We then link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side
+Next, we link the client-side `appear` method to `AppearanceChannel#appear(data)`. This is possible because the server-side
channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these
can be reached as remote procedure calls via a subscription's `perform` method.
@@ -215,7 +215,7 @@ ActionCable.server.broadcast \
"web_notifications_#{current_user.id}", { title: 'New things!', body: 'All the news that is fit to print' }
```
-The `ActionCable.server.broadcast` call places a message in the Redis' pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications_1`.
+The `ActionCable.server.broadcast` call places a message in the Action Cable pubsub queue under a separate broadcasting name for each user. For a user with an ID of 1, the broadcasting name would be `web_notifications_1`.
The channel has been instructed to stream everything that arrives at `web_notifications_1` directly to the client by invoking the
`#received(data)` callback. The data is the hash sent as the second parameter to the server-side broadcast call, JSON encoded for the trip
across the wire, and unpacked for the data argument arriving to `#received`.
@@ -234,7 +234,7 @@ class ChatChannel < ApplicationCable::Channel
end
```
-Pass an object as the first argument to `subscriptions.create`, and that object will become your params hash in your cable channel. The keyword `channel` is required.
+If you pass an object as the first argument to `subscriptions.create`, that object will become the params hash in your cable channel. The keyword `channel` is required.
```coffeescript
# Client-side, which assumes you've already requested the right to send web notifications
@@ -293,7 +293,7 @@ The rebroadcast will be received by all connected clients, _including_ the clien
### More complete examples
-See the [rails/actioncable-examples](http://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app and adding channels.
+See the [rails/actioncable-examples](http://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app, and how to add channels.
## Configuration
@@ -339,21 +339,21 @@ Rails.application.config.action_cable.disable_request_forgery_protection = true
### Consumer Configuration
-Once you have decided how to run your cable server (see below), you must provide the server url (or path) to your client-side setup.
+Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup.
There are two ways you can do this.
The first is to simply pass it in when creating your consumer. For a standalone server,
this would be something like: `App.cable = ActionCable.createConsumer("ws://example.com:28080")`, and for an in-app server,
something like: `App.cable = ActionCable.createConsumer("/cable")`.
-The second option is to pass the server url through the `action_cable_meta_tag` in your layout.
-This uses a url or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable".
+The second option is to pass the server URL through the `action_cable_meta_tag` in your layout.
+This uses a URL or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable".
-This method is especially useful if your websocket url might change between environments. If you host your production server via https, you will need to use the wss scheme
-for your ActionCable server, but development might remain http and use the ws scheme. You might use localhost in development and your
+This method is especially useful if your WebSocket URL might change between environments. If you host your production server via https, you will need to use the wss scheme
+for your Action Cable server, but development might remain http and use the ws scheme. You might use localhost in development and your
domain in production.
-In any case, to vary the websocket url between environments, add the following configuration to each environment:
+In any case, to vary the WebSocket URL between environments, add the following configuration to each environment:
```ruby
config.action_cable.url = "ws://example.com:28080"
@@ -412,16 +412,16 @@ The above will start a cable server on port 28080.
### In app
-If you are using a threaded server like Puma or Thin, the current implementation of ActionCable can run side-along with your Rails application. For example, to listen for WebSocket requests on `/cable`, mount the server at that path:
+If you are using a server that supports the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`:
```ruby
-# config/routes.rb
-Example::Application.routes.draw do
- mount ActionCable.server => '/cable'
+# config/application.rb
+class Application < Rails::Application
+ config.action_cable.mount_path = '/websocket'
end
```
-For every instance of your server you create and for every worker your server spawns, you will also have a new instance of ActionCable, but the use of Redis keeps messages synced across connections.
+For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis keeps messages synced across connections.
### Notes
@@ -433,24 +433,20 @@ The WebSocket server doesn't have access to the session, but it has access to th
## Dependencies
-Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, evented Redis, and non-evented Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. To create your own adapter, you can look at `ActionCable::SubscriptionAdapter::Base` for all methods that must be implemented, and any of the adapters included within ActionCable as example implementations.
+Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, evented Redis, and non-evented Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. To create your own adapter, you can look at `ActionCable::SubscriptionAdapter::Base` for all methods that must be implemented, and any of the adapters included within Action Cable as example implementations.
The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), [nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby).
## Deployment
-Action Cable is powered by a combination of websockets and threads. All of the
+Action Cable is powered by a combination of WebSockets and threads. All of the
connection management is handled internally by utilizing Ruby’s native thread
support, which means you can use all your regular Rails models with no problems
as long as you haven’t committed any thread-safety sins.
-But this also means that Action Cable needs to run in its own server process.
-So you'll have one set of server processes for your normal web work, and another
-set of server processes for the Action Cable.
-
The Action Cable server does _not_ need to be a multi-threaded application server.
-This is because Action Cable uses the [Rack socket hijacking API](http://old.blog.phusion.nl/2013/01/23/the-new-rack-socket-hijacking-api/)
+This is because Action Cable uses the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking)
to take over control of connections from the application server. Action Cable
then manages connections internally, in a multithreaded manner, regardless of
whether the application server is multi-threaded or not. So Action Cable works
diff --git a/actioncable/Rakefile b/actioncable/Rakefile
index 1d77fc7067..5ba7b7f7f6 100644
--- a/actioncable/Rakefile
+++ b/actioncable/Rakefile
@@ -19,6 +19,14 @@ Rake::TestTask.new do |t|
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
+namespace :test do
+ task :isolated do
+ Dir.glob("test/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
+ end or raise "Failures"
+ end
+end
+
namespace :assets do
root_path = Pathname.new(dir)
destination_path = root_path.join("lib/assets/compiled")
diff --git a/actioncable/app/assets/javascripts/action_cable.coffee.erb b/actioncable/app/assets/javascripts/action_cable.coffee.erb
index 18a48c0610..f0422d9d9c 100644
--- a/actioncable/app/assets/javascripts/action_cable.coffee.erb
+++ b/actioncable/app/assets/javascripts/action_cable.coffee.erb
@@ -4,7 +4,8 @@
@ActionCable =
INTERNAL: <%= ActionCable::INTERNAL.to_json %>
- createConsumer: (url = @getConfig("url")) ->
+ createConsumer: (url) ->
+ url ?= @getConfig("url") ? @INTERNAL.default_mount_path
new ActionCable.Consumer @createWebSocketURL(url)
getConfig: (name) ->
@@ -21,3 +22,14 @@
a.href
else
url
+
+ startDebugging: ->
+ @debugging = true
+
+ stopDebugging: ->
+ @debugging = null
+
+ log: (messages...) ->
+ if @debugging
+ messages.push(Date.now())
+ console.log("[ActionCable]", messages...)
diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee
index fbd7dbd35b..d6a6397804 100644
--- a/actioncable/app/assets/javascripts/action_cable/connection.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee
@@ -1,12 +1,17 @@
+#= require ./connection_monitor
+
# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
-{message_types} = ActionCable.INTERNAL
+{message_types, protocols} = ActionCable.INTERNAL
+[supportedProtocols..., unsupportedProtocol] = protocols
class ActionCable.Connection
@reopenDelay: 500
constructor: (@consumer) ->
- @open()
+ {@subscriptions} = @consumer
+ @monitor = new ActionCable.ConnectionMonitor this
+ @disconnected = true
send: (data) ->
if @isOpen()
@@ -16,30 +21,48 @@ class ActionCable.Connection
false
open: =>
- if @webSocket and not @isState("closed")
+ if @isActive()
+ ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}")
throw new Error("Existing connection must be closed before opening")
else
- @webSocket = new WebSocket(@consumer.url)
+ ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}")
+ @uninstallEventHandlers() if @webSocket?
+ @webSocket = new WebSocket(@consumer.url, protocols)
@installEventHandlers()
+ @monitor.start()
true
- close: ->
- @webSocket?.close()
+ close: ({allowReconnect} = {allowReconnect: true}) ->
+ @monitor.stop() unless allowReconnect
+ @webSocket?.close() if @isActive()
reopen: ->
- if @isState("closed")
- @open()
- else
+ ActionCable.log("Reopening WebSocket, current state is #{@getState()}")
+ if @isActive()
try
@close()
+ catch error
+ ActionCable.log("Failed to reopen WebSocket", error)
finally
+ ActionCable.log("Reopening WebSocket in #{@constructor.reopenDelay}ms")
setTimeout(@open, @constructor.reopenDelay)
+ else
+ @open()
+
+ getProtocol: ->
+ @webSocket?.protocol
isOpen: ->
@isState("open")
+ isActive: ->
+ @isState("open", "connecting")
+
# Private
+ isProtocolSupported: ->
+ @getProtocol() in supportedProtocols
+
isState: (states...) ->
@getState() in states
@@ -53,29 +76,41 @@ class ActionCable.Connection
@webSocket["on#{eventName}"] = handler
return
+ uninstallEventHandlers: ->
+ for eventName of @events
+ @webSocket["on#{eventName}"] = ->
+ return
+
events:
message: (event) ->
+ return unless @isProtocolSupported()
{identifier, message, type} = JSON.parse(event.data)
-
switch type
+ when message_types.welcome
+ @monitor.recordConnect()
+ @subscriptions.reload()
+ when message_types.ping
+ @monitor.recordPing()
when message_types.confirmation
- @consumer.subscriptions.notify(identifier, "connected")
+ @subscriptions.notify(identifier, "connected")
when message_types.rejection
- @consumer.subscriptions.reject(identifier)
+ @subscriptions.reject(identifier)
else
- @consumer.subscriptions.notify(identifier, "received", message)
+ @subscriptions.notify(identifier, "received", message)
open: ->
+ ActionCable.log("WebSocket onopen event, using '#{@getProtocol()}' subprotocol")
@disconnected = false
- @consumer.subscriptions.reload()
+ if not @isProtocolSupported()
+ ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.")
+ @close(allowReconnect: false)
- close: ->
- @disconnect()
+ close: (event) ->
+ ActionCable.log("WebSocket onclose event")
+ return if @disconnected
+ @disconnected = true
+ @monitor.recordDisconnect()
+ @subscriptions.notifyAll("disconnected", {willAttemptReconnect: @monitor.isRunning()})
error: ->
- @disconnect()
-
- disconnect: ->
- return if @disconnected
- @disconnected = true
- @consumer.subscriptions.notifyAll("disconnected")
+ ActionCable.log("WebSocket onerror event")
diff --git a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee
index 99b9a1c6d5..0cc675fa94 100644
--- a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee
@@ -7,54 +7,69 @@ class ActionCable.ConnectionMonitor
@staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
- identifier: ActionCable.INTERNAL.identifiers.ping
+ constructor: (@connection) ->
+ @reconnectAttempts = 0
- constructor: (@consumer) ->
- @consumer.subscriptions.add(this)
- @start()
+ start: ->
+ unless @isRunning()
+ @startedAt = now()
+ delete @stoppedAt
+ @startPolling()
+ document.addEventListener("visibilitychange", @visibilityDidChange)
+ ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms")
- connected: ->
- @reset()
- @pingedAt = now()
- delete @disconnectedAt
+ stop: ->
+ if @isRunning()
+ @stoppedAt = now()
+ @stopPolling()
+ document.removeEventListener("visibilitychange", @visibilityDidChange)
+ ActionCable.log("ConnectionMonitor stopped")
- disconnected: ->
- @disconnectedAt = now()
+ isRunning: ->
+ @startedAt? and not @stoppedAt?
- received: ->
+ recordPing: ->
@pingedAt = now()
- reset: ->
+ recordConnect: ->
@reconnectAttempts = 0
+ @recordPing()
+ delete @disconnectedAt
+ ActionCable.log("ConnectionMonitor recorded connect")
- start: ->
- @reset()
- delete @stoppedAt
- @startedAt = now()
+ recordDisconnect: ->
+ @disconnectedAt = now()
+ ActionCable.log("ConnectionMonitor recorded disconnect")
+
+ # Private
+
+ startPolling: ->
+ @stopPolling()
@poll()
- document.addEventListener("visibilitychange", @visibilityDidChange)
- stop: ->
- @stoppedAt = now()
- document.removeEventListener("visibilitychange", @visibilityDidChange)
+ stopPolling: ->
+ clearTimeout(@pollTimeout)
poll: ->
- setTimeout =>
- unless @stoppedAt
- @reconnectIfStale()
- @poll()
- , @getInterval()
+ @pollTimeout = setTimeout =>
+ @reconnectIfStale()
+ @poll()
+ , @getPollInterval()
- getInterval: ->
+ getPollInterval: ->
{min, max} = @constructor.pollInterval
interval = 5 * Math.log(@reconnectAttempts + 1)
- clamp(interval, min, max) * 1000
+ Math.round(clamp(interval, min, max) * 1000)
reconnectIfStale: ->
if @connectionIsStale()
+ ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = #{@reconnectAttempts}, pollInterval = #{@getPollInterval()} ms, time disconnected = #{secondsSince(@disconnectedAt)} s, stale threshold = #{@constructor.staleThreshold} s")
@reconnectAttempts++
- unless @disconnectedRecently()
- @consumer.connection.reopen()
+ if @disconnectedRecently()
+ ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
+ else
+ ActionCable.log("ConnectionMonitor reopening")
+ @connection.reopen()
connectionIsStale: ->
secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold
@@ -65,8 +80,9 @@ class ActionCable.ConnectionMonitor
visibilityDidChange: =>
if document.visibilityState is "visible"
setTimeout =>
- if @connectionIsStale() or not @consumer.connection.isOpen()
- @consumer.connection.reopen()
+ if @connectionIsStale() or not @connection.isOpen()
+ ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = #{document.visibilityState}")
+ @connection.reopen()
, 200
now = ->
diff --git a/actioncable/app/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee
index 717c0641a9..3298be717f 100644
--- a/actioncable/app/assets/javascripts/action_cable/consumer.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/consumer.coffee
@@ -1,5 +1,4 @@
#= require ./connection
-#= require ./connection_monitor
#= require ./subscriptions
#= require ./subscription
@@ -15,11 +14,33 @@
# App.appearance = App.cable.subscriptions.create "AppearanceChannel"
#
# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
+#
+# When a consumer is created, it automatically connects with the server.
+#
+# To disconnect from the server, call
+#
+# App.cable.disconnect()
+#
+# and to restart the connection:
+#
+# App.cable.connect()
+#
+# Any channel subscriptions which existed prior to disconnecting will
+# automatically resubscribe.
class ActionCable.Consumer
constructor: (@url) ->
@subscriptions = new ActionCable.Subscriptions this
@connection = new ActionCable.Connection this
- @connectionMonitor = new ActionCable.ConnectionMonitor this
send: (data) ->
@connection.send(data)
+
+ connect: ->
+ @connection.open()
+
+ disconnect: ->
+ @connection.close(allowReconnect: false)
+
+ ensureActiveConnection: ->
+ unless @connection.isActive()
+ @connection.open()
diff --git a/actioncable/app/assets/javascripts/action_cable/subscription.coffee b/actioncable/app/assets/javascripts/action_cable/subscription.coffee
index 339d676933..8e0805a174 100644
--- a/actioncable/app/assets/javascripts/action_cable/subscription.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/subscription.coffee
@@ -1,5 +1,5 @@
-# A new subscription is created through the ActionCable.Subscriptions instance available on the consumer.
-# It provides a number of callbacks and a method for calling remote procedure calls on the corresponding
+# A new subscription is created through the ActionCable.Subscriptions instance available on the consumer.
+# It provides a number of callbacks and a method for calling remote procedure calls on the corresponding
# Channel instance on the server side.
#
# An example demonstrates the basic functionality:
@@ -7,13 +7,19 @@
# App.appearance = App.cable.subscriptions.create "AppearanceChannel",
# connected: ->
# # Called once the subscription has been successfully completed
-#
+#
+# disconnected: ({ willAttemptReconnect: boolean }) ->
+# # Called when the client has disconnected with the server.
+# # The object will have an `willAttemptReconnect` property which
+# # says whether the client has the intention of attempting
+# # to reconnect.
+#
# appear: ->
# @perform 'appear', appearing_on: @appearingOn()
-#
+#
# away: ->
# @perform 'away'
-#
+#
# appearingOn: ->
# $('main').data 'appearing-on'
#
@@ -27,15 +33,15 @@
# def subscribed
# current_user.appear
# end
-#
+#
# def unsubscribed
# current_user.disappear
# end
-#
+#
# def appear(data)
# current_user.appear on: data['appearing_on']
# end
-#
+#
# def away
# current_user.away
# end
@@ -44,11 +50,9 @@
# The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name.
# The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the @perform method.
class ActionCable.Subscription
- constructor: (@subscriptions, params = {}, mixin) ->
+ constructor: (@consumer, params = {}, mixin) ->
@identifier = JSON.stringify(params)
extend(this, mixin)
- @subscriptions.add(this)
- @consumer = @subscriptions.consumer
# Perform a channel action with the optional data passed as an attribute
perform: (action, data = {}) ->
@@ -59,7 +63,7 @@ class ActionCable.Subscription
@consumer.send(command: "message", identifier: @identifier, data: JSON.stringify(data))
unsubscribe: ->
- @subscriptions.remove(this)
+ @consumer.subscriptions.remove(this)
extend = (object, properties) ->
if properties?
diff --git a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee
index ae041ffa2b..aa052bf5d8 100644
--- a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee
+++ b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee
@@ -13,28 +13,33 @@ class ActionCable.Subscriptions
create: (channelName, mixin) ->
channel = channelName
params = if typeof channel is "object" then channel else {channel}
- new ActionCable.Subscription this, params, mixin
+ subscription = new ActionCable.Subscription @consumer, params, mixin
+ @add(subscription)
# Private
add: (subscription) ->
@subscriptions.push(subscription)
+ @consumer.ensureActiveConnection()
@notify(subscription, "initialized")
@sendCommand(subscription, "subscribe")
+ subscription
remove: (subscription) ->
@forget(subscription)
-
unless @findAll(subscription.identifier).length
@sendCommand(subscription, "unsubscribe")
+ subscription
reject: (identifier) ->
for subscription in @findAll(identifier)
@forget(subscription)
@notify(subscription, "rejected")
+ subscription
forget: (subscription) ->
@subscriptions = (s for s in @subscriptions when s isnt subscription)
+ subscription
findAll: (identifier) ->
s for s in @subscriptions when s.identifier is identifier
@@ -58,7 +63,4 @@ class ActionCable.Subscriptions
sendCommand: (subscription, command) ->
{identifier} = subscription
- if identifier is ActionCable.INTERNAL.identifiers.ping
- @consumer.connection.isOpen()
- else
- @consumer.send({command, identifier})
+ @consumer.send({command, identifier})
diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb
index 1dc66ef3ad..b6d2842867 100644
--- a/actioncable/lib/action_cable.rb
+++ b/actioncable/lib/action_cable.rb
@@ -29,13 +29,14 @@ module ActionCable
extend ActiveSupport::Autoload
INTERNAL = {
- identifiers: {
- ping: '_ping'.freeze
- },
message_types: {
+ welcome: 'welcome'.freeze,
+ ping: 'ping'.freeze,
confirmation: 'confirm_subscription'.freeze,
rejection: 'reject_subscription'.freeze
- }
+ },
+ default_mount_path: '/cable'.freeze,
+ protocols: ["actioncable-v1-json".freeze, "actioncable-unsupported".freeze].freeze
}
# Singleton instance of the server
diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb
index 874ebe2e71..845b747fc5 100644
--- a/actioncable/lib/action_cable/channel/base.rb
+++ b/actioncable/lib/action_cable/channel/base.rb
@@ -32,8 +32,8 @@ module ActionCable
#
# == Action processing
#
- # Unlike subclasses of ActionController::Base, channels do not follow a REST
- # constraint form for their actions. Instead, ActionCable operates through a
+ # Unlike subclasses of ActionController::Base, channels do not follow a RESTful
+ # constraint form for their actions. Instead, Action Cable operates through a
# remote-procedure call model. You can declare any public method on the
# channel (optionally taking a <tt>data</tt> argument), and this method is
# automatically exposed as callable to the client.
@@ -63,10 +63,10 @@ module ActionCable
# end
# end
#
- # In this example, subscribed/unsubscribed are not callable methods, as they
+ # In this example, the subscribed and unsubscribed methods are not callable methods, as they
# were already declared in ActionCable::Channel::Base, but <tt>#appear</tt>
# and <tt>#away</tt> are. <tt>#generate_connection_token</tt> is also not
- # callable as it's a private method. You'll see that appear accepts a data
+ # callable, since it's a private method. You'll see that appear accepts a data
# parameter, which it then uses as part of its model call. <tt>#away</tt>
# does not, since it's simply a trigger action.
#
@@ -125,7 +125,7 @@ module ActionCable
protected
# action_methods are cached and there is sometimes need to refresh
# them. ::clear_action_methods! allows you to do that, so next time
- # you run action_methods, they will be recalculated
+ # you run action_methods, they will be recalculated.
def clear_action_methods!
@action_methods = nil
end
@@ -160,15 +160,18 @@ module ActionCable
action = extract_action(data)
if processable_action?(action)
- dispatch_action(action, data)
+ payload = { channel_class: self.class.name, action: action, data: data }
+ ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
+ dispatch_action(action, data)
+ end
else
logger.error "Unable to process #{action_signature(action, data)}"
end
end
- # Called by the cable connection when its cut so the channel has a chance to cleanup with callbacks.
+ # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
# This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
- def unsubscribe_from_channel
+ def unsubscribe_from_channel # :nodoc:
run_callbacks :unsubscribe do
unsubscribed
end
@@ -183,7 +186,7 @@ module ActionCable
end
# Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
- # people as offline or the like.
+ # users as offline or the like.
def unsubscribed
# Override in subclasses
end
@@ -191,8 +194,12 @@ module ActionCable
# Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with
# the proper channel identifier marked as the recipient.
def transmit(data, via: nil)
- logger.info "#{self.class.name} transmitting #{data.inspect}".tap { |m| m << " (via #{via})" if via }
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data)
+ logger.info "#{self.class.name} transmitting #{data.inspect.truncate(300)}".tap { |m| m << " (via #{via})" if via }
+
+ payload = { channel_class: self.class.name, data: data, via: via }
+ ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
+ connection.transmit identifier: @identifier, message: data
+ end
end
def defer_subscription_confirmation!
@@ -224,7 +231,6 @@ module ActionCable
end
end
-
def subscribe_to_channel
run_callbacks :subscribe do
subscribed
@@ -237,7 +243,6 @@ module ActionCable
end
end
-
def extract_action(data)
(data['action'].presence || :receive).to_sym
end
@@ -267,8 +272,11 @@ module ActionCable
def transmit_subscription_confirmation
unless subscription_confirmation_sent?
logger.info "#{self.class.name} is transmitting the subscription confirmation"
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation])
- @subscription_confirmation_sent = true
+
+ ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]
+ @subscription_confirmation_sent = true
+ end
end
end
@@ -279,7 +287,10 @@ module ActionCable
def transmit_subscription_rejection
logger.info "#{self.class.name} is transmitting the subscription rejection"
- connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection])
+
+ ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do
+ connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]
+ end
end
end
end
diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb
index 56597d02d7..dab604440f 100644
--- a/actioncable/lib/action_cable/channel/periodic_timers.rb
+++ b/actioncable/lib/action_cable/channel/periodic_timers.rb
@@ -12,11 +12,42 @@ module ActionCable
end
module ClassMethods
- # Allow you to call a private method <tt>every</tt> so often seconds. This periodic timer can be useful
- # for sending a steady flow of updates to a client based off an object that was configured on subscription.
- # It's an alternative to using streams if the channel is able to do the work internally.
- def periodically(callback, every:)
- self.periodic_timers += [ [ callback, every: every ] ]
+ # Periodically performs a task on the channel, like updating an online
+ # user counter, polling a backend for new status messages, sending
+ # regular "heartbeat" messages, or doing some internal work and giving
+ # progress updates.
+ #
+ # Pass a method name or lambda argument or provide a block to call.
+ # Specify the calling period in seconds using the <tt>every:</tt>
+ # keyword argument.
+ #
+ # periodically :transmit_progress, every: 5.seconds
+ #
+ # periodically every: 3.minutes do
+ # transmit action: :update_count, count: current_count
+ # end
+ #
+ def periodically(callback_or_method_name = nil, every:, &block)
+ callback =
+ if block_given?
+ raise ArgumentError, 'Pass a block or provide a callback arg, not both' if callback_or_method_name
+ block
+ else
+ case callback_or_method_name
+ when Proc
+ callback_or_method_name
+ when Symbol
+ -> { __send__ callback_or_method_name }
+ else
+ raise ArgumentError, "Expected a Symbol method name or a Proc, got #{callback_or_method_name.inspect}"
+ end
+ end
+
+ unless every.kind_of?(Numeric) && every > 0
+ raise ArgumentError, "Expected every: to be a positive number of seconds, got #{every.inspect}"
+ end
+
+ self.periodic_timers += [[ callback, every: every ]]
end
end
@@ -27,14 +58,21 @@ module ActionCable
def start_periodic_timers
self.class.periodic_timers.each do |callback, options|
- active_periodic_timers << Concurrent::TimerTask.new(execution_interval: options[:every]) do
- connection.worker_pool.async_run_periodic_timer(self, callback)
+ active_periodic_timers << start_periodic_timer(callback, every: options.fetch(:every))
+ end
+ end
+
+ def start_periodic_timer(callback, every:)
+ connection.server.event_loop.timer every do
+ connection.worker_pool.async_invoke connection do
+ instance_exec(&callback)
end
end
end
def stop_periodic_timers
active_periodic_timers.each { |timer| timer.shutdown }
+ active_periodic_timers.clear
end
end
end
diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb
index 3158f30814..84594fb3d6 100644
--- a/actioncable/lib/action_cable/channel/streams.rb
+++ b/actioncable/lib/action_cable/channel/streams.rb
@@ -1,8 +1,8 @@
module ActionCable
module Channel
- # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pub/sub queue where any data
- # put into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not
- # streaming a broadcasting at the very moment it sends out an update, you'll not get that update when connecting later.
+ # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data
+ # placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not
+ # streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent.
#
# Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between
# the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new
@@ -18,8 +18,10 @@ module ActionCable
# end
# end
#
- # So the subscribers of this channel will get whatever data is put into the, let's say, `comments_for_45` broadcasting as soon as it's put there.
- # That looks like so from that side of things:
+ # Based on the above example, the subscribers of this channel will get whatever data is put into the,
+ # let's say, `comments_for_45` broadcasting as soon as it's put there.
+ #
+ # An example broadcasting for this channel looks like so:
#
# ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell'
#
@@ -37,16 +39,14 @@ module ActionCable
#
# CommentsChannel.broadcast_to(@post, @comment)
#
- # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can supply a callback that lets you alter what goes out.
- # Example below shows how you can use this to provide performance introspection in the process:
+ # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can also supply a callback that lets you alter what is sent out.
+ # The below example shows how you can use this to provide performance introspection in the process:
#
# class ChatChannel < ApplicationCable::Channel
# def subscribed
# @room = Chat::Room[params[:room_number]]
#
- # stream_for @room, -> (encoded_message) do
- # message = ActiveSupport::JSON.decode(encoded_message)
- #
+ # stream_for @room, coder: ActiveSupport::JSON do |message|
# if message['originated_at'].present?
# elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
#
@@ -69,15 +69,26 @@ module ActionCable
# Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
# instead of the default of just transmitting the updates straight to the subscriber.
- def stream_from(broadcasting, callback = nil)
- # Hold off the confirmation until pubsub#subscribe is successful
+ # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback.
+ # Defaults to `coder: nil` which does no decoding, passes raw messages.
+ def stream_from(broadcasting, callback = nil, coder: nil, &block)
+ broadcasting = String(broadcasting)
+ # Don't send the confirmation until pubsub#subscribe is successful
defer_subscription_confirmation!
- callback ||= default_stream_callback(broadcasting)
- streams << [ broadcasting, callback ]
+ if user_handler = callback || block
+ user_handler = -> message { handler.(coder.decode(message)) } if coder
+ handler = -> message do
+ connection.worker_pool.async_invoke(user_handler, :call, message)
+ end
+ else
+ handler = default_stream_handler(broadcasting, coder: coder)
+ end
+
+ streams << [ broadcasting, handler ]
- Concurrent.global_io_executor.post do
- pubsub.subscribe(broadcasting, callback, lambda do
+ connection.server.event_loop.post do
+ pubsub.subscribe(broadcasting, handler, lambda do
transmit_subscription_confirmation
logger.info "#{self.class.name} is streaming from #{broadcasting}"
end)
@@ -87,8 +98,11 @@ module ActionCable
# Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a
# <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
# to the subscriber.
- def stream_for(model, callback = nil)
- stream_from(broadcasting_for([ channel_name, model ]), callback)
+ #
+ # Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback.
+ # Defaults to `coder: nil` 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)
end
# Unsubscribes all streams associated with this channel from the pubsub queue.
@@ -106,9 +120,11 @@ module ActionCable
@_streams ||= []
end
- def default_stream_callback(broadcasting)
+ def default_stream_handler(broadcasting, coder:)
+ coder ||= ActiveSupport::JSON
+
-> (message) do
- transmit ActiveSupport::JSON.decode(message), via: "streamed from #{broadcasting}"
+ transmit coder.decode(message), via: "streamed from #{broadcasting}"
end
end
end
diff --git a/actioncable/lib/action_cable/connection.rb b/actioncable/lib/action_cable/connection.rb
index 902efb07e2..5f813cf8e0 100644
--- a/actioncable/lib/action_cable/connection.rb
+++ b/actioncable/lib/action_cable/connection.rb
@@ -8,6 +8,8 @@ module ActionCable
autoload :ClientSocket
autoload :Identification
autoload :InternalChannel
+ autoload :FayeClientSocket
+ autoload :FayeEventLoop
autoload :MessageBuffer
autoload :Stream
autoload :StreamEventLoop
diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb
index e23789978c..cc4e0f8c8b 100644
--- a/actioncable/lib/action_cable/connection/base.rb
+++ b/actioncable/lib/action_cable/connection/base.rb
@@ -2,9 +2,9 @@ require 'action_dispatch'
module ActionCable
module Connection
- # For every WebSocket the cable server is accepting, a Connection object will be instantiated. This instance becomes the parent
- # of all the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
- # based on an identifier sent by the cable consumer. The Connection itself does not deal with any specific application logic beyond
+ # For every WebSocket the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
+ # of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
+ # based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
# authentication and authorization.
#
# Here's a basic example:
@@ -33,14 +33,14 @@ module ActionCable
# end
# end
#
- # First, we declare that this connection can be identified by its current_user. This allows us later to be able to find all connections
- # established for that current_user (and potentially disconnect them if the user was removed from an account). You can declare as many
- # identification indexes as you like. Declaring an identification means that a attr_accessor is automatically set for that key.
+ # First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections
+ # established for that current_user (and potentially disconnect them). You can declare as many
+ # identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key.
#
# Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes
# it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection.
#
- # Finally, we add a tag to the connection-specific logger with name of the current user to easily distinguish their messages in the log.
+ # Finally, we add a tag to the connection-specific logger with the name of the current user to easily distinguish their messages in the log.
#
# Pretty simple, eh?
class Base
@@ -48,15 +48,16 @@ module ActionCable
include InternalChannel
include Authorization
- attr_reader :server, :env, :subscriptions, :logger
- delegate :stream_event_loop, :worker_pool, :pubsub, to: :server
+ attr_reader :server, :env, :subscriptions, :logger, :worker_pool, :protocol
+ delegate :event_loop, :pubsub, to: :server
- def initialize(server, env)
- @server, @env = server, env
+ def initialize(server, env, coder: ActiveSupport::JSON)
+ @server, @env, @coder = server, env, coder
+ @worker_pool = server.worker_pool
@logger = new_tagged_logger
- @websocket = ActionCable::Connection::WebSocket.new(env, self, stream_event_loop)
+ @websocket = ActionCable::Connection::WebSocket.new(env, self, event_loop, server.config.client_socket_class)
@subscriptions = ActionCable::Connection::Subscriptions.new(self)
@message_buffer = ActionCable::Connection::MessageBuffer.new(self)
@@ -65,8 +66,8 @@ module ActionCable
end
# Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
- # This method should not be called directly. Rely on the #connect (and #disconnect) callback instead.
- def process
+ # This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
+ def process #:nodoc:
logger.info started_request_message
if websocket.possible? && allow_request_origin?
@@ -76,20 +77,22 @@ module ActionCable
end
end
- # Data received over the cable is handled by this method. It's expected that everything inbound is JSON encoded.
- # The data is routed to the proper channel that the connection has subscribed to.
- def receive(data_in_json)
+ # Decodes WebSocket messages and dispatches them to subscribed channels.
+ # WebSocket message transfer encoding is always JSON.
+ def receive(websocket_message) #:nodoc:
+ send_async :dispatch_websocket_message, websocket_message
+ end
+
+ def dispatch_websocket_message(websocket_message) #:nodoc:
if websocket.alive?
- subscriptions.execute_command ActiveSupport::JSON.decode(data_in_json)
+ subscriptions.execute_command decode(websocket_message)
else
- logger.error "Received data without a live WebSocket (#{data_in_json.inspect})"
+ logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})"
end
end
- # Send raw data straight back down the WebSocket. This is not intended to be called directly. Use the #transmit available on the
- # Channel instead, as that'll automatically address the correct subscriber and wrap the message in JSON.
- def transmit(data)
- websocket.transmit data
+ def transmit(cable_message) # :nodoc:
+ websocket.transmit encode(cable_message)
end
# Close the WebSocket connection.
@@ -114,7 +117,7 @@ module ActionCable
end
def beat
- transmit ActiveSupport::JSON.encode(identifier: ActionCable::INTERNAL[:identifiers][:ping], message: Time.now.to_i)
+ transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i
end
def on_open # :nodoc:
@@ -151,10 +154,19 @@ module ActionCable
attr_reader :message_buffer
private
+ def encode(cable_message)
+ @coder.encode cable_message
+ end
+
+ def decode(websocket_message)
+ @coder.decode websocket_message
+ end
+
def handle_open
+ @protocol = websocket.protocol
connect if respond_to?(:connect)
subscribe_to_internal_channel
- beat
+ send_welcome_message
message_buffer.process!
server.add_connection(self)
@@ -173,6 +185,13 @@ module ActionCable
disconnect if respond_to?(:disconnect)
end
+ def send_welcome_message
+ # Send welcome message to the internal connection monitor channel.
+ # This ensures the connection monitor state is reset after a successful
+ # websocket connection.
+ transmit type: ActionCable::INTERNAL[:message_types][:welcome]
+ end
+
def allow_request_origin?
return true if server.config.disable_request_forgery_protection
diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb
index ef937d7c16..6f29f32ea9 100644
--- a/actioncable/lib/action_cable/connection/client_socket.rb
+++ b/actioncable/lib/action_cable/connection/client_socket.rb
@@ -29,10 +29,10 @@ module ActionCable
attr_reader :env, :url
- def initialize(env, event_target, stream_event_loop)
- @env = env
- @event_target = event_target
- @stream_event_loop = stream_event_loop
+ def initialize(env, event_target, event_loop, protocols)
+ @env = env
+ @event_target = event_target
+ @event_loop = event_loop
@url = ClientSocket.determine_url(@env)
@@ -42,22 +42,24 @@ module ActionCable
@ready_state = CONNECTING
# The driver calls +env+, +url+, and +write+
- @driver = ::WebSocket::Driver.rack(self)
+ @driver = ::WebSocket::Driver.rack(self, protocols: protocols)
@driver.on(:open) { |e| open }
@driver.on(:message) { |e| receive_message(e.data) }
@driver.on(:close) { |e| begin_close(e.reason, e.code) }
@driver.on(:error) { |e| emit_error(e.message) }
- @stream = ActionCable::Connection::Stream.new(@stream_event_loop, self)
+ @stream = ActionCable::Connection::Stream.new(@event_loop, self)
+ end
+
+ def start_driver
+ return if @driver.nil? || @driver_started
+ @stream.hijack_rack_socket
if callback = @env['async.callback']
callback.call([101, {}, @stream])
end
- end
- def start_driver
- return if @driver.nil? || @driver_started
@driver_started = true
@driver.start
end
@@ -69,6 +71,8 @@ module ActionCable
def write(data)
@stream.write(data)
+ rescue => e
+ emit_error e.message
end
def transmit(message)
@@ -107,6 +111,10 @@ module ActionCable
@ready_state == OPEN
end
+ def protocol
+ @driver.protocol
+ end
+
private
def open
return unless @ready_state == CONNECTING
@@ -132,11 +140,8 @@ module ActionCable
@ready_state = CLOSING
@close_params = [reason, code]
- if @stream
- @stream.shutdown
- else
- finalize_close
- end
+ @stream.shutdown if @stream
+ finalize_close
end
def finalize_close
diff --git a/actioncable/lib/action_cable/connection/faye_client_socket.rb b/actioncable/lib/action_cable/connection/faye_client_socket.rb
new file mode 100644
index 0000000000..a4bfe7db17
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/faye_client_socket.rb
@@ -0,0 +1,48 @@
+require 'faye/websocket'
+
+module ActionCable
+ module Connection
+ class FayeClientSocket
+ def initialize(env, event_target, stream_event_loop, protocols)
+ @env = env
+ @event_target = event_target
+ @protocols = protocols
+
+ @faye = nil
+ end
+
+ def alive?
+ @faye && @faye.ready_state == Faye::WebSocket::API::OPEN
+ end
+
+ def transmit(data)
+ connect
+ @faye.send data
+ end
+
+ def close
+ @faye && @faye.close
+ end
+
+ def protocol
+ @faye && @faye.protocol
+ end
+
+ def rack_response
+ connect
+ @faye.rack_response
+ end
+
+ private
+ def connect
+ return if @faye
+ @faye = Faye::WebSocket.new(@env, @protocols)
+
+ @faye.on(:open) { |event| @event_target.on_open }
+ @faye.on(:message) { |event| @event_target.on_message(event.data) }
+ @faye.on(:close) { |event| @event_target.on_close(event.reason, event.code) }
+ @faye.on(:error) { |event| @event_target.on_error(event.message) }
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/faye_event_loop.rb b/actioncable/lib/action_cable/connection/faye_event_loop.rb
new file mode 100644
index 0000000000..9c44b38bc3
--- /dev/null
+++ b/actioncable/lib/action_cable/connection/faye_event_loop.rb
@@ -0,0 +1,44 @@
+require 'thread'
+
+require 'eventmachine'
+EventMachine.epoll if EventMachine.epoll?
+EventMachine.kqueue if EventMachine.kqueue?
+
+module ActionCable
+ module Connection
+ class FayeEventLoop
+ @@mutex = Mutex.new
+
+ def timer(interval, &block)
+ ensure_reactor_running
+ EMTimer.new(::EM::PeriodicTimer.new(interval, &block))
+ end
+
+ def post(task = nil, &block)
+ task ||= block
+
+ ensure_reactor_running
+ ::EM.next_tick(&task)
+ end
+
+ private
+ def ensure_reactor_running
+ return if EventMachine.reactor_running?
+ @@mutex.synchronize do
+ Thread.new { EventMachine.run } unless EventMachine.reactor_running?
+ Thread.pass until EventMachine.reactor_running?
+ end
+ end
+
+ class EMTimer
+ def initialize(inner)
+ @inner = inner
+ end
+
+ def shutdown
+ @inner.cancel
+ end
+ end
+ end
+ end
+end
diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb
index 885ff3f102..4a54044aff 100644
--- a/actioncable/lib/action_cable/connection/identification.rb
+++ b/actioncable/lib/action_cable/connection/identification.rb
@@ -12,7 +12,7 @@ module ActionCable
class_methods do
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
- # Common identifiers are current_user and current_account, but could be anything really.
+ # Common identifiers are current_user and current_account, but could be anything, really.
#
# Note that anything marked as an identifier will automatically create a delegate by the same name on any
# channel instances created off the connection.
diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb
index 27826792b3..f70d52f99b 100644
--- a/actioncable/lib/action_cable/connection/internal_channel.rb
+++ b/actioncable/lib/action_cable/connection/internal_channel.rb
@@ -11,24 +11,22 @@ module ActionCable
def subscribe_to_internal_channel
if connection_identifier.present?
- callback = -> (message) { process_internal_message(message) }
+ callback = -> (message) { process_internal_message decode(message) }
@_internal_subscriptions ||= []
@_internal_subscriptions << [ internal_channel, callback ]
- Concurrent.global_io_executor.post { pubsub.subscribe(internal_channel, callback) }
+ server.event_loop.post { pubsub.subscribe(internal_channel, callback) }
logger.info "Registered connection (#{connection_identifier})"
end
end
def unsubscribe_from_internal_channel
if @_internal_subscriptions.present?
- @_internal_subscriptions.each { |channel, callback| Concurrent.global_io_executor.post { pubsub.unsubscribe(channel, callback) } }
+ @_internal_subscriptions.each { |channel, callback| server.event_loop.post { pubsub.unsubscribe(channel, callback) } }
end
end
def process_internal_message(message)
- message = ActiveSupport::JSON.decode(message)
-
case message['type']
when 'disconnect'
logger.info "Removing connection (#{connection_identifier})"
diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb
index 2f65a1e84a..6a80770cae 100644
--- a/actioncable/lib/action_cable/connection/message_buffer.rb
+++ b/actioncable/lib/action_cable/connection/message_buffer.rb
@@ -1,8 +1,7 @@
module ActionCable
module Connection
- # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized and is ready to receive them.
- # Entirely internal operation and should not be used directly by the user.
- class MessageBuffer
+ # Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them.
+ class MessageBuffer # :nodoc:
def initialize(connection)
@connection = connection
@buffered_messages = []
@@ -31,7 +30,7 @@ module ActionCable
protected
attr_reader :connection
- attr_accessor :buffered_messages
+ attr_reader :buffered_messages
private
def valid?(message)
@@ -39,7 +38,7 @@ module ActionCable
end
def receive(message)
- connection.send_async :receive, message
+ connection.receive message
end
def buffer(message)
diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb
index ace250cd16..0cf59091bc 100644
--- a/actioncable/lib/action_cable/connection/stream.rb
+++ b/actioncable/lib/action_cable/connection/stream.rb
@@ -4,15 +4,13 @@ module ActionCable
# This class is heavily based on faye-websocket-ruby
#
# Copyright (c) 2010-2015 James Coglan
- class Stream
+ class Stream # :nodoc:
def initialize(event_loop, socket)
@event_loop = event_loop
@socket_object = socket
@stream_send = socket.env['stream.send']
@rack_hijack_io = nil
-
- hijack_rack_socket
end
def each(&callback)
@@ -31,7 +29,7 @@ module ActionCable
def write(data)
return @rack_hijack_io.write(data) if @rack_hijack_io
return @stream_send.call(data) if @stream_send
- rescue EOFError
+ rescue EOFError, Errno::ECONNRESET
@socket_object.client_gone
end
@@ -39,16 +37,16 @@ module ActionCable
@socket_object.parse(data)
end
- private
- def hijack_rack_socket
- return unless @socket_object.env['rack.hijack']
+ def hijack_rack_socket
+ return unless @socket_object.env['rack.hijack']
- @socket_object.env['rack.hijack'].call
- @rack_hijack_io = @socket_object.env['rack.hijack_io']
+ @socket_object.env['rack.hijack'].call
+ @rack_hijack_io = @socket_object.env['rack.hijack_io']
- @event_loop.attach(@rack_hijack_io, self)
- end
+ @event_loop.attach(@rack_hijack_io, self)
+ end
+ private
def clean_rack_hijack
return unless @rack_hijack_io
@event_loop.detach(@rack_hijack_io, self)
diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb
index e6335082d2..2abad09c03 100644
--- a/actioncable/lib/action_cable/connection/stream_event_loop.rb
+++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb
@@ -11,7 +11,16 @@ module ActionCable
@todo = Queue.new
@spawn_mutex = Mutex.new
- spawn
+ end
+
+ def timer(interval, &block)
+ Concurrent::TimerTask.new(execution_interval: interval, &block).tap(&:execute)
+ end
+
+ def post(task = nil, &block)
+ task ||= block
+
+ Concurrent.global_io_executor << task
end
def attach(io, stream)
diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb
index d7f95e6a62..3742f248d1 100644
--- a/actioncable/lib/action_cable/connection/subscriptions.rb
+++ b/actioncable/lib/action_cable/connection/subscriptions.rb
@@ -3,8 +3,8 @@ require 'active_support/core_ext/hash/indifferent_access'
module ActionCable
module Connection
# Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on
- # the connection to the proper channel. Should not be used directly by the user.
- class Subscriptions
+ # the connection to the proper channel.
+ class Subscriptions # :nodoc:
def initialize(connection)
@connection = connection
@subscriptions = {}
@@ -54,7 +54,7 @@ module ActionCable
end
def unsubscribe_from_all
- subscriptions.each { |id, channel| channel.unsubscribe_from_channel }
+ subscriptions.each { |id, channel| remove_subscription(channel) }
end
protected
diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb
index 5e89fb9b72..11f28c37e8 100644
--- a/actioncable/lib/action_cable/connection/web_socket.rb
+++ b/actioncable/lib/action_cable/connection/web_socket.rb
@@ -4,8 +4,8 @@ module ActionCable
module Connection
# Wrap the real socket to minimize the externally-presented API
class WebSocket
- def initialize(env, event_target, stream_event_loop)
- @websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, stream_event_loop) : nil
+ def initialize(env, event_target, event_loop, client_socket_class, protocols: ActionCable::INTERNAL[:protocols])
+ @websocket = ::WebSocket::Driver.websocket?(env) ? client_socket_class.new(env, event_target, event_loop, protocols) : nil
end
def possible?
@@ -24,6 +24,10 @@ module ActionCable
websocket.close
end
+ def protocol
+ websocket.protocol
+ end
+
def rack_response
websocket.rack_response
end
diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb
index f5e233e091..7dc541d00c 100644
--- a/actioncable/lib/action_cable/engine.rb
+++ b/actioncable/lib/action_cable/engine.rb
@@ -6,7 +6,7 @@ require "active_support/core_ext/hash/indifferent_access"
module ActionCable
class Railtie < Rails::Engine # :nodoc:
config.action_cable = ActiveSupport::OrderedOptions.new
- config.action_cable.url = '/cable'
+ config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
config.eager_load_namespaces << ActionCable
@@ -31,8 +31,50 @@ module ActionCable
self.cable = Rails.application.config_for(config_path).with_indifferent_access
end
+ if 'ApplicationCable::Connection'.safe_constantize
+ self.connection_class = ApplicationCable::Connection
+ end
+
+ self.channel_paths = Rails.application.paths['app/channels'].existent
+
options.each { |k,v| send("#{k}=", v) }
end
end
+
+ initializer "action_cable.routes" do
+ config.after_initialize do |app|
+ config = app.config
+ unless config.action_cable.mount_path.nil?
+ app.routes.prepend do
+ mount ActionCable.server => config.action_cable.mount_path, internal: true
+ end
+ end
+ end
+ end
+
+ initializer "action_cable.set_work_hooks" do |app|
+ ActiveSupport.on_load(:action_cable) do
+ ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
+ app.executor.wrap do
+ # If we took a while to get the lock, we may have been halted
+ # in the meantime. As we haven't started doing any real work
+ # yet, we should pretend that we never made it off the queue.
+ unless stopping?
+ inner.call
+ end
+ end
+ end
+
+ wrap = lambda do |_, inner|
+ app.executor.wrap(&inner)
+ end
+ ActionCable::Channel::Base.set_callback :subscribe, :around, prepend: true, &wrap
+ ActionCable::Channel::Base.set_callback :unsubscribe, :around, prepend: true, &wrap
+
+ app.reloader.before_class_unload do
+ ActionCable.server.restart
+ end
+ end
+ end
end
end
diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb
index a71603e61a..67adeefaff 100644
--- a/actioncable/lib/action_cable/gem_version.rb
+++ b/actioncable/lib/action_cable/gem_version.rb
@@ -8,7 +8,7 @@ module ActionCable
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/actioncable/lib/action_cable/helpers/action_cable_helper.rb b/actioncable/lib/action_cable/helpers/action_cable_helper.rb
index b82751468a..2081a37db6 100644
--- a/actioncable/lib/action_cable/helpers/action_cable_helper.rb
+++ b/actioncable/lib/action_cable/helpers/action_cable_helper.rb
@@ -1,28 +1,39 @@
module ActionCable
module Helpers
module ActionCableHelper
- # Returns an "action-cable-url" meta tag with the value of the url specified in your
- # configuration. Ensure this is above your javascript tag:
+ # Returns an "action-cable-url" meta tag with the value of the URL specified in your
+ # configuration. Ensure this is above your JavaScript tag:
#
# <head>
# <%= action_cable_meta_tag %>
# <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
# </head>
#
- # This is then used by ActionCable to determine the url of your websocket server.
+ # This is then used by Action Cable to determine the URL of your WebSocket server.
# Your CoffeeScript can then connect to the server without needing to specify the
- # url directly:
+ # URL directly:
#
# #= require cable
# @App = {}
# App.cable = Cable.createConsumer()
#
- # Make sure to specify the correct server location in each of your environments
- # config file:
+ # Make sure to specify the correct server location in each of your environment
+ # config files:
+ #
+ # config.action_cable.mount_path = "/cable123"
+ # <%= action_cable_meta_tag %> would render:
+ # => <meta name="action-cable-url" content="/cable123" />
+ #
+ # config.action_cable.url = "ws://actioncable.com"
+ # <%= action_cable_meta_tag %> would render:
+ # => <meta name="action-cable-url" content="ws://actioncable.com" />
#
- # config.action_cable.url = "ws://example.com:28080"
def action_cable_meta_tag
- tag "meta", name: "action-cable-url", content: Rails.application.config.action_cable.url
+ tag "meta", name: "action-cable-url", content: (
+ ActionCable.server.config.url ||
+ ActionCable.server.config.mount_path ||
+ raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
+ )
end
end
end
diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb
index 7ec121308a..aeef8abc72 100644
--- a/actioncable/lib/action_cable/remote_connections.rb
+++ b/actioncable/lib/action_cable/remote_connections.rb
@@ -13,8 +13,8 @@ module ActionCable
# ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
#
# This will disconnect all the connections established for
- # <tt>User.find(1)</tt> across all servers running on all machines, because
- # it uses the internal channel that all these servers are subscribed to.
+ # <tt>User.find(1)</tt>, across all servers running on all machines, because
+ # it uses the internal channel that all of these servers are subscribed to.
class RemoteConnections
attr_reader :server
diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb
index fe48c112df..b1a0e11631 100644
--- a/actioncable/lib/action_cable/server/base.rb
+++ b/actioncable/lib/action_cable/server/base.rb
@@ -1,11 +1,11 @@
-require 'thread'
+require 'monitor'
module ActionCable
module Server
- # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the rack process that starts the cable server, but
- # also by the user to reach the RemoteConnections instead for finding and disconnecting connections across all servers.
+ # A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
+ # is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
#
- # Also, this is the server instance used for broadcasting. See Broadcasting for details.
+ # Also, this is the server instance used for broadcasting. See Broadcasting for more information.
class Base
include ActionCable::Server::Broadcasting
include ActionCable::Server::Connections
@@ -18,12 +18,11 @@ module ActionCable
attr_reader :mutex
def initialize
- @mutex = Mutex.new
-
- @remote_connections = @stream_event_loop = @worker_pool = @channel_classes = @pubsub = nil
+ @mutex = Monitor.new
+ @remote_connections = @event_loop = @worker_pool = @channel_classes = @pubsub = nil
end
- # Called by rack to setup the server.
+ # Called by Rack to setup the server.
def call(env)
setup_heartbeat_timer
config.connection_class.new(self, env).process
@@ -34,21 +33,41 @@ module ActionCable
remote_connections.where(identifiers).disconnect
end
+ def restart
+ connections.each(&:close)
+
+ @mutex.synchronize do
+ worker_pool.halt if @worker_pool
+
+ @worker_pool = nil
+ end
+ end
+
# Gateway to RemoteConnections. See that class for details.
def remote_connections
@remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
end
- def stream_event_loop
- @stream_event_loop || @mutex.synchronize { @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new }
+ def event_loop
+ @event_loop || @mutex.synchronize { @event_loop ||= config.event_loop_class.new }
end
- # The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size.
+ # The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
+ # The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out
+ # at 4 worker threads by default. Tune the size yourself with config.action_cable.worker_pool_size.
+ #
+ # Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
+ # Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
+ # connections.
+ #
+ # Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
+ # the db connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
+ # db connection pool instead.
def worker_pool
@worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
end
- # Requires and returns a hash of all the channel class constants keyed by name.
+ # Requires and returns a hash of all of the channel class constants, which are keyed by name.
def channel_classes
@channel_classes || @mutex.synchronize do
@channel_classes ||= begin
@@ -63,7 +82,7 @@ module ActionCable
@pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
end
- # All the identifiers applied to the connection class associated with this server.
+ # All of the identifiers applied to the connection class associated with this server.
def connection_identifiers
config.connection_class.identifiers
end
diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb
index 7e8aef45f4..8f93564113 100644
--- a/actioncable/lib/action_cable/server/broadcasting.rb
+++ b/actioncable/lib/action_cable/server/broadcasting.rb
@@ -1,6 +1,6 @@
module ActionCable
module Server
- # Broadcasting is how other parts of your application can send messages to the channel subscribers. As explained in Channel, most of the time, these
+ # Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these
# broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
#
# class WebNotificationsChannel < ApplicationCable::Channel
@@ -9,37 +9,38 @@ module ActionCable
# end
# end
#
- # # Somewhere in your app this is called, perhaps from a NewCommentJob
+ # # Somewhere in your app this is called, perhaps from a NewCommentJob:
# ActionCable.server.broadcast \
# "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
#
- # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications
+ # # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications:
# App.cable.subscriptions.create "WebNotificationsChannel",
# received: (data) ->
# new Notification data['title'], body: data['body']
module Broadcasting
- # Broadcast a hash directly to a named <tt>broadcasting</tt>. It'll automatically be JSON encoded.
- def broadcast(broadcasting, message)
- broadcaster_for(broadcasting).broadcast(message)
+ # Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
+ def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
+ broadcaster_for(broadcasting, coder: coder).broadcast(message)
end
- # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have a object that
+ # Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
# may need multiple spots to transmit to a specific broadcasting over and over.
- def broadcaster_for(broadcasting)
- Broadcaster.new(self, broadcasting)
+ def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
+ Broadcaster.new(self, String(broadcasting), coder: coder)
end
private
class Broadcaster
- attr_reader :server, :broadcasting
+ attr_reader :server, :broadcasting, :coder
- def initialize(server, broadcasting)
- @server, @broadcasting = server, broadcasting
+ def initialize(server, broadcasting, coder:)
+ @server, @broadcasting, @coder = server, broadcasting, coder
end
def broadcast(message)
- server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}"
- server.pubsub.broadcast broadcasting, ActiveSupport::JSON.encode(message)
+ server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
+ encoded = coder ? coder.encode(message) : message
+ server.pubsub.broadcast broadcasting, encoded
end
end
end
diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb
index 9a248933c4..0bb378cf03 100644
--- a/actioncable/lib/action_cable/server/configuration.rb
+++ b/actioncable/lib/action_cable/server/configuration.rb
@@ -1,31 +1,24 @@
module ActionCable
module Server
- # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak the configuration points
+ # An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
# in a Rails config initializer.
class Configuration
attr_accessor :logger, :log_tags
- attr_accessor :connection_class, :worker_pool_size
- attr_accessor :channel_load_paths
+ attr_accessor :use_faye, :connection_class, :worker_pool_size
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
- attr_accessor :cable, :url
+ attr_accessor :cable, :url, :mount_path
+
+ attr_accessor :channel_paths # :nodoc:
def initialize
@log_tags = []
- @connection_class = ApplicationCable::Connection
- @worker_pool_size = 100
-
- @channel_load_paths = [Rails.root.join('app/channels')]
+ @connection_class = ActionCable::Connection::Base
+ @worker_pool_size = 4
@disable_request_forgery_protection = false
end
- def channel_paths
- @channel_paths ||= channel_load_paths.flat_map do |path|
- Dir["#{path}/**/*_channel.rb"]
- end
- end
-
def channel_class_names
@channel_class_names ||= channel_paths.collect do |channel_path|
Pathname.new(channel_path).basename.to_s.split('.').first.camelize
@@ -50,6 +43,22 @@ module ActionCable
adapter = 'PostgreSQL' if adapter == 'Postgresql'
"ActionCable::SubscriptionAdapter::#{adapter}".constantize
end
+
+ def event_loop_class
+ if use_faye
+ ActionCable::Connection::FayeEventLoop
+ else
+ ActionCable::Connection::StreamEventLoop
+ end
+ end
+
+ def client_socket_class
+ if use_faye
+ ActionCable::Connection::FayeClientSocket
+ else
+ ActionCable::Connection::ClientSocket
+ end
+ end
end
end
end
diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb
index 8671dd5ebd..5e61b4e335 100644
--- a/actioncable/lib/action_cable/server/connections.rb
+++ b/actioncable/lib/action_cable/server/connections.rb
@@ -1,9 +1,8 @@
module ActionCable
module Server
- # Collection class for all the connections that's been established on this specific server. Remember, usually you'll run many cable servers, so
- # you can't use this collection as an full list of all the connections established against your application. Use RemoteConnections for that.
- # As such, this is primarily for internal use.
- module Connections
+ # Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
+ # you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
+ module Connections # :nodoc:
BEAT_INTERVAL = 3
def connections
@@ -19,12 +18,12 @@ module ActionCable
end
# WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you
- # then can't rely on being able to receive and send to it. So there's a 3 second heartbeat running on all connections. If the beat fails, we automatically
+ # then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
# disconnect.
def setup_heartbeat_timer
- @heartbeat_timer ||= Concurrent::TimerTask.new(execution_interval: BEAT_INTERVAL) do
- Concurrent.global_io_executor.post { connections.map(&:beat) }
- end.tap(&:execute)
+ @heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
+ event_loop.post { connections.map(&:beat) }
+ end
end
def open_connections_statistics
diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb
index 3b6c6d44a1..46a8989f34 100644
--- a/actioncable/lib/action_cable/server/worker.rb
+++ b/actioncable/lib/action_cable/server/worker.rb
@@ -4,8 +4,8 @@ require 'concurrent'
module ActionCable
module Server
- # Worker used by Server.send_async to do connection work in threads. Only for internal use.
- class Worker
+ # Worker used by Server.send_async to do connection work in threads.
+ class Worker # :nodoc:
include ActiveSupport::Callbacks
thread_mattr_accessor :connection
@@ -20,44 +20,42 @@ module ActionCable
)
end
- def async_invoke(receiver, method, *args)
- @pool.post do
- invoke(receiver, method, *args)
- end
+ # Stop processing work: any work that has not already started
+ # running will be discarded from the queue
+ def halt
+ @pool.kill
end
- def invoke(receiver, method, *args)
- begin
- self.connection = receiver
+ def stopping?
+ @pool.shuttingdown?
+ end
- run_callbacks :work do
- receiver.send method, *args
- end
- rescue Exception => e
- logger.error "There was an exception - #{e.class}(#{e.message})"
- logger.error e.backtrace.join("\n")
+ def work(connection)
+ self.connection = connection
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
- ensure
- self.connection = nil
+ run_callbacks :work do
+ yield
end
+ ensure
+ self.connection = nil
end
- def async_run_periodic_timer(channel, callback)
+ def async_invoke(receiver, method, *args)
@pool.post do
- run_periodic_timer(channel, callback)
+ invoke(receiver, method, *args)
end
end
- def run_periodic_timer(channel, callback)
- begin
- self.connection = channel.connection
+ def invoke(receiver, method, *args)
+ work(receiver) do
+ begin
+ receiver.send method, *args
+ rescue Exception => e
+ logger.error "There was an exception - #{e.class}(#{e.message})"
+ logger.error e.backtrace.join("\n")
- run_callbacks :work do
- callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback)
+ receiver.handle_exception if receiver.respond_to?(:handle_exception)
end
- ensure
- self.connection = nil
end
end
diff --git a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
index ecece4e270..c1e4aa8103 100644
--- a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
+++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb
@@ -1,7 +1,6 @@
module ActionCable
module Server
class Worker
- # Clear active connections between units of work so the long-running channel or connection processes do not hoard connections.
module ActiveRecordConnectionManagement
extend ActiveSupport::Concern
@@ -13,10 +12,8 @@ module ActionCable
def with_database_connections
connection.logger.tag(ActiveRecord::Base.logger) { yield }
- ensure
- ActiveRecord::Base.clear_active_connections!
end
end
end
end
-end \ No newline at end of file
+end
diff --git a/actioncable/lib/action_cable/subscription_adapter/async.rb b/actioncable/lib/action_cable/subscription_adapter/async.rb
index cca6894289..10b3ac8cd8 100644
--- a/actioncable/lib/action_cable/subscription_adapter/async.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/async.rb
@@ -5,16 +5,21 @@ module ActionCable
class Async < Inline # :nodoc:
private
def new_subscriber_map
- AsyncSubscriberMap.new
+ AsyncSubscriberMap.new(server.event_loop)
end
class AsyncSubscriberMap < SubscriberMap
+ def initialize(event_loop)
+ @event_loop = event_loop
+ super()
+ end
+
def add_subscriber(*)
- Concurrent.global_io_executor.post { super }
+ @event_loop.post { super }
end
def invoke_callback(*)
- Concurrent.global_io_executor.post { super }
+ @event_loop.post { super }
end
end
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
index af04a58c70..256876cf30 100644
--- a/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/evented_redis.rb
@@ -13,11 +13,11 @@ module ActionCable
class EventedRedis < Base # :nodoc:
@@mutex = Mutex.new
- # Overwrite this factory method for EventMachine redis connections if you want to use a different Redis library than EM::Hiredis.
+ # Overwrite this factory method for EventMachine Redis connections if you want to use a different Redis connection library than EM::Hiredis.
# This is needed, for example, when using Makara proxies for distributed Redis.
cattr_accessor(:em_redis_connector) { ->(config) { EM::Hiredis.connect(config[:url]) } }
- # Overwrite this factory method for redis connections if you want to use a different Redis library than Redis.
+ # Overwrite this factory method for Redis connections if you want to use a different Redis connection library than Redis.
# This is needed, for example, when using Makara proxies for distributed Redis.
cattr_accessor(:redis_connector) { ->(config) { ::Redis.new(url: config[:url]) } }
@@ -51,7 +51,7 @@ module ActionCable
@redis_connection_for_subscriptions || @server.mutex.synchronize do
@redis_connection_for_subscriptions ||= self.class.em_redis_connector.call(@server.config.cable).tap do |redis|
redis.on(:reconnect_failed) do
- @logger.info "[ActionCable] Redis reconnect failed."
+ @logger.error "[ActionCable] Redis reconnect failed."
end
end
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
index abaeb92e54..66c7852f6e 100644
--- a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb
@@ -42,14 +42,15 @@ module ActionCable
private
def listener
- @listener || @server.mutex.synchronize { @listener ||= Listener.new(self) }
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
end
class Listener < SubscriberMap
- def initialize(adapter)
+ def initialize(adapter, event_loop)
super()
@adapter = adapter
+ @event_loop = event_loop
@queue = Queue.new
@thread = Thread.new do
@@ -68,7 +69,7 @@ module ActionCable
case action
when :listen
pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}")
- Concurrent.global_io_executor << callback if callback
+ @event_loop.post(&callback) if callback
when :unlisten
pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}")
when :shutdown
@@ -98,7 +99,7 @@ module ActionCable
end
def invoke_callback(*)
- Concurrent.global_io_executor.post { super }
+ @event_loop.post { super }
end
end
end
diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb
index ba4934a264..65434f7107 100644
--- a/actioncable/lib/action_cable/subscription_adapter/redis.rb
+++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb
@@ -33,25 +33,30 @@ module ActionCable
end
def redis_connection_for_subscriptions
- ::Redis.new(@server.config.cable)
+ redis_connection
end
private
def listener
- @listener || @server.mutex.synchronize { @listener ||= Listener.new(self) }
+ @listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
end
def redis_connection_for_broadcasts
@redis_connection_for_broadcasts || @server.mutex.synchronize do
- @redis_connection_for_broadcasts ||= self.class.redis_connector.call(@server.config.cable)
+ @redis_connection_for_broadcasts ||= redis_connection
end
end
+ def redis_connection
+ self.class.redis_connector.call(@server.config.cable)
+ end
+
class Listener < SubscriberMap
- def initialize(adapter)
+ def initialize(adapter, event_loop)
super()
@adapter = adapter
+ @event_loop = event_loop
@subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
@subscription_lock = Mutex.new
@@ -80,7 +85,7 @@ module ActionCable
if callbacks = @subscribe_callbacks[chan]
next_callback = callbacks.shift
- Concurrent.global_io_executor << next_callback if next_callback
+ @event_loop.post(&next_callback) if next_callback
@subscribe_callbacks.delete(chan) if callbacks.empty?
end
end
@@ -129,7 +134,7 @@ module ActionCable
end
def invoke_callback(*)
- Concurrent.global_io_executor.post { super }
+ @event_loop.post { super }
end
private
diff --git a/actioncable/lib/rails/generators/channel/USAGE b/actioncable/lib/rails/generators/channel/USAGE
index 27a934c689..6249553c22 100644
--- a/actioncable/lib/rails/generators/channel/USAGE
+++ b/actioncable/lib/rails/generators/channel/USAGE
@@ -3,7 +3,7 @@ Description:
Stubs out a new cable channel for the server (in Ruby) and client (in CoffeeScript).
Pass the channel name, either CamelCased or under_scored, and an optional list of channel actions as arguments.
- Note: Turn on the cable connection in app/assets/javascript/cable.coffee after generating any channels.
+ Note: Turn on the cable connection in app/assets/javascript/cable.js after generating any channels.
Example:
========
diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb
index c5d398810a..05fd21a954 100644
--- a/actioncable/lib/rails/generators/channel/channel_generator.rb
+++ b/actioncable/lib/rails/generators/channel/channel_generator.rb
@@ -13,13 +13,33 @@ module Rails
template "channel.rb", File.join('app/channels', class_path, "#{file_name}_channel.rb")
if options[:assets]
+ if self.behavior == :invoke
+ template "assets/cable.js", "app/assets/javascripts/cable.js"
+ end
template "assets/channel.coffee", File.join('app/assets/javascripts/channels', class_path, "#{file_name}.coffee")
end
+
+ generate_application_cable_files
end
protected
def file_name
- @_file_name ||= super.gsub(/\_channel/i, '')
+ @_file_name ||= super.gsub(/_channel/i, '')
+ end
+
+ # FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
+ def generate_application_cable_files
+ return if self.behavior != :invoke
+
+ files = [
+ 'application_cable/channel.rb',
+ 'application_cable/connection.rb'
+ ]
+
+ files.each do |name|
+ path = File.join('app/channels/', name)
+ template(name, path) if !File.exist?(path)
+ end
end
end
end
diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb
new file mode 100644
index 0000000000..d56fa30f4d
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb
new file mode 100644
index 0000000000..b4f41389ad
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb
@@ -0,0 +1,5 @@
+# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/actioncable/lib/rails/generators/channel/templates/assets/cable.js b/actioncable/lib/rails/generators/channel/templates/assets/cable.js
new file mode 100644
index 0000000000..71ee1e66de
--- /dev/null
+++ b/actioncable/lib/rails/generators/channel/templates/assets/cable.js
@@ -0,0 +1,13 @@
+// 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.
+//
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+ this.App || (this.App = {});
+
+ App.cable = ActionCable.createConsumer();
+
+}).call(this);
diff --git a/actioncable/test/channel/base_test.rb b/actioncable/test/channel/base_test.rb
index d41bf3064b..daa782eeb3 100644
--- a/actioncable/test/channel/base_test.rb
+++ b/actioncable/test/channel/base_test.rb
@@ -146,12 +146,12 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
test "transmitting data" do
@channel.perform_action 'action' => :get_latest
- expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "message" => { "data" => "latest" }
+ expected = { "identifier" => "{id: 1}", "message" => { "data" => "latest" }}
assert_equal expected, @connection.last_transmission
end
test "subscription confirmation" do
- expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "confirm_subscription"
+ expected = { "identifier" => "{id: 1}", "type" => "confirm_subscription" }
assert_equal expected, @connection.last_transmission
end
@@ -166,6 +166,81 @@ class ActionCable::Channel::BaseTest < ActiveSupport::TestCase
end
end
+ test "notification for perform_action" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe "perform_action.action_cable" do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ data = {'action' => :speak, 'content' => 'hello'}
+ @channel.perform_action data
+
+ assert_equal 1, events.length
+ assert_equal 'perform_action.action_cable', events[0].name
+ assert_equal 'ActionCable::Channel::BaseTest::ChatChannel', events[0].payload[:channel_class]
+ assert_equal :speak, events[0].payload[:action]
+ assert_equal data, events[0].payload[:data]
+ ensure
+ ActiveSupport::Notifications.unsubscribe "perform_action.action_cable"
+ end
+ end
+
+ test "notification for transmit" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe 'transmit.action_cable' do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ @channel.perform_action 'action' => :get_latest
+ expected_data = {data: 'latest'}
+
+ assert_equal 1, events.length
+ assert_equal 'transmit.action_cable', events[0].name
+ assert_equal 'ActionCable::Channel::BaseTest::ChatChannel', events[0].payload[:channel_class]
+ assert_equal expected_data, events[0].payload[:data]
+ assert_nil events[0].payload[:via]
+ ensure
+ ActiveSupport::Notifications.unsubscribe 'transmit.action_cable'
+ end
+ end
+
+ test "notification for transmit_subscription_confirmation" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe 'transmit_subscription_confirmation.action_cable' do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ @channel.stubs(:subscription_confirmation_sent?).returns(false)
+ @channel.send(:transmit_subscription_confirmation)
+
+ assert_equal 1, events.length
+ assert_equal 'transmit_subscription_confirmation.action_cable', events[0].name
+ assert_equal 'ActionCable::Channel::BaseTest::ChatChannel', events[0].payload[:channel_class]
+ ensure
+ ActiveSupport::Notifications.unsubscribe 'transmit_subscription_confirmation.action_cable'
+ end
+ end
+
+ test "notification for transmit_subscription_rejection" do
+ begin
+ events = []
+ ActiveSupport::Notifications.subscribe 'transmit_subscription_rejection.action_cable' do |*args|
+ events << ActiveSupport::Notifications::Event.new(*args)
+ end
+
+ @channel.send(:transmit_subscription_rejection)
+
+ assert_equal 1, events.length
+ assert_equal 'transmit_subscription_rejection.action_cable', events[0].name
+ assert_equal 'ActionCable::Channel::BaseTest::ChatChannel', events[0].payload[:channel_class]
+ ensure
+ ActiveSupport::Notifications.unsubscribe 'transmit_subscription_rejection.action_cable'
+ end
+ end
+
private
def assert_logged(message)
old_logger = @connection.logger
diff --git a/actioncable/test/channel/periodic_timers_test.rb b/actioncable/test/channel/periodic_timers_test.rb
index 64f0247cd6..03464003cf 100644
--- a/actioncable/test/channel/periodic_timers_test.rb
+++ b/actioncable/test/channel/periodic_timers_test.rb
@@ -1,12 +1,21 @@
require 'test_helper'
require 'stubs/test_connection'
require 'stubs/room'
+require 'active_support/time'
class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase
class ChatChannel < ActionCable::Channel::Base
- periodically -> { ping }, every: 5
+ # Method name arg
periodically :send_updates, every: 1
+ # Proc arg
+ periodically -> { ping }, every: 2
+
+ # Block arg
+ periodically every: 3 do
+ ping
+ end
+
private
def ping
end
@@ -19,22 +28,41 @@ class ActionCable::Channel::PeriodicTimersTest < ActiveSupport::TestCase
test "periodic timers definition" do
timers = ChatChannel.periodic_timers
- assert_equal 2, timers.size
+ assert_equal 3, timers.size
- first_timer = timers[0]
- assert_kind_of Proc, first_timer[0]
- assert_equal 5, first_timer[1][:every]
+ timers.each_with_index do |timer, i|
+ assert_kind_of Proc, timer[0]
+ assert_equal i+1, timer[1][:every]
+ end
+ end
- second_timer = timers[1]
- assert_equal :send_updates, second_timer[0]
- assert_equal 1, second_timer[1][:every]
+ test 'disallow negative and zero periods' do
+ [ 0, 0.0, 0.seconds, -1, -1.seconds, 'foo', :foo, Object.new ].each do |invalid|
+ assert_raise ArgumentError, /Expected every:/ do
+ ChatChannel.periodically :send_updates, every: invalid
+ end
+ end
+ end
+
+ test 'disallow block and arg together' do
+ assert_raise ArgumentError, /not both/ do
+ ChatChannel.periodically(:send_updates, every: 1) { ping }
+ end
+ end
+
+ test 'disallow unknown args' do
+ [ 'send_updates', Object.new, nil ].each do |invalid|
+ assert_raise ArgumentError, /Expected a Symbol/ do
+ ChatChannel.periodically invalid, every: 1
+ end
+ end
end
test "timer start and stop" do
- Concurrent::TimerTask.expects(:new).times(2).returns(true)
+ @connection.server.event_loop.expects(:timer).times(3).returns(stub(shutdown: nil))
channel = ChatChannel.new @connection, "{id: 1}", { id: 1 }
- channel.expects(:stop_periodic_timers).once
channel.unsubscribe_from_channel
+ assert_equal [], channel.send(:active_periodic_timers)
end
end
diff --git a/actioncable/test/channel/rejection_test.rb b/actioncable/test/channel/rejection_test.rb
index aa93396d44..15db57d6ba 100644
--- a/actioncable/test/channel/rejection_test.rb
+++ b/actioncable/test/channel/rejection_test.rb
@@ -18,7 +18,7 @@ class ActionCable::Channel::RejectionTest < ActiveSupport::TestCase
@connection.expects(:subscriptions).returns mock().tap { |m| m.expects(:remove_subscription).with instance_of(SecretChannel) }
@channel = SecretChannel.new @connection, "{id: 1}", { id: 1 }
- expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "reject_subscription"
+ expected = { "identifier" => "{id: 1}", "type" => "reject_subscription" }
assert_equal expected, @connection.last_transmission
end
diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb
index 947efd96d4..df129a7c62 100644
--- a/actioncable/test/channel/stream_test.rb
+++ b/actioncable/test/channel/stream_test.rb
@@ -2,12 +2,20 @@ require 'test_helper'
require 'stubs/test_connection'
require 'stubs/room'
-class ActionCable::Channel::StreamTest < ActionCable::TestCase
+module ActionCable::StreamTests
+ class Connection < ActionCable::Connection::Base
+ attr_reader :websocket
+
+ def send_async(method, *args)
+ send method, *args
+ end
+ end
+
class ChatChannel < ActionCable::Channel::Base
def subscribed
if params[:id]
@room = Room.new params[:id]
- stream_from "test_room_#{@room.id}"
+ stream_from "test_room_#{@room.id}", coder: pick_coder(params[:coder])
end
end
@@ -15,60 +23,160 @@ class ActionCable::Channel::StreamTest < ActionCable::TestCase
transmit_subscription_confirmation
end
+ private def pick_coder(coder)
+ case coder
+ when nil, 'json'
+ ActiveSupport::JSON
+ when 'custom'
+ DummyEncoder
+ when 'none'
+ nil
+ end
+ end
end
- test "streaming start and stop" do
- run_in_eventmachine do
- connection = TestConnection.new
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
- channel = ChatChannel.new connection, "{id: 1}", { id: 1 }
+ module DummyEncoder
+ extend self
+ def encode(*) '{ "foo": "encoded" }' end
+ def decode(*) { foo: 'decoded' } end
+ end
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
- channel.unsubscribe_from_channel
+ class SymbolChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from :channel
end
end
- test "stream_for" do
- run_in_eventmachine do
- connection = TestConnection.new
- connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:channel:stream_test:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
+ class StreamTest < ActionCable::TestCase
+ test "streaming start and stop" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
+ channel = ChatChannel.new connection, "{id: 1}", { id: 1 }
- channel = ChatChannel.new connection, ""
- channel.stream_for Room.new(1)
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
+ channel.unsubscribe_from_channel
+ end
end
- end
- test "stream_from subscription confirmation" do
- run_in_eventmachine do
- connection = TestConnection.new
+ test "stream from non-string channel" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("channel", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
+ channel = SymbolChannel.new connection, ""
- ChatChannel.new connection, "{id: 1}", { id: 1 }
- assert_nil connection.last_transmission
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe) }
+ channel.unsubscribe_from_channel
+ end
+ end
- wait_for_async
+ test "stream_for" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+ connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:stream_tests:chat:Room#1-Campfire", kind_of(Proc), kind_of(Proc)).returns stub_everything(:pubsub) }
- expected = ActiveSupport::JSON.encode "identifier" => "{id: 1}", "type" => "confirm_subscription"
- connection.transmit(expected)
+ channel = ChatChannel.new connection, ""
+ channel.stream_for Room.new(1)
+ end
+ end
+
+ test "stream_from subscription confirmation" do
+ run_in_eventmachine do
+ connection = TestConnection.new
+
+ ChatChannel.new connection, "{id: 1}", { id: 1 }
+ assert_nil connection.last_transmission
+
+ wait_for_async
+
+ confirmation = { "identifier" => "{id: 1}", "type" => "confirm_subscription" }
+ connection.transmit(confirmation)
- assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation within 0.1s"
+ assert_equal confirmation, connection.last_transmission, "Did not receive subscription confirmation within 0.1s"
+ end
end
- end
- test "subscription confirmation should only be sent out once" do
- run_in_eventmachine do
- connection = TestConnection.new
+ test "subscription confirmation should only be sent out once" do
+ run_in_eventmachine do
+ connection = TestConnection.new
- channel = ChatChannel.new connection, "test_channel"
- channel.send_confirmation
- channel.send_confirmation
+ channel = ChatChannel.new connection, "test_channel"
+ channel.send_confirmation
+ channel.send_confirmation
- wait_for_async
+ wait_for_async
- expected = ActiveSupport::JSON.encode "identifier" => "test_channel", "type" => "confirm_subscription"
- assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation"
+ expected = { "identifier" => "test_channel", "type" => "confirm_subscription" }
+ assert_equal expected, connection.last_transmission, "Did not receive subscription confirmation"
- assert_equal 1, connection.transmissions.size
+ assert_equal 1, connection.transmissions.size
+ end
end
end
+ require 'action_cable/subscription_adapter/inline'
+
+ class UserCallbackChannel < ActionCable::Channel::Base
+ def subscribed
+ stream_from :channel do
+ Thread.current[:ran_callback] = true
+ end
+ end
+ end
+
+ class StreamEncodingTest < ActionCable::TestCase
+ setup do
+ @server = TestServer.new(subscription_adapter: ActionCable::SubscriptionAdapter::Inline)
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ @server.stubs(:channel_classes).returns(
+ ChatChannel.name => ChatChannel,
+ UserCallbackChannel.name => UserCallbackChannel,
+ )
+ end
+
+ test 'custom encoder' do
+ run_in_eventmachine do
+ connection = open_connection
+ subscribe_to connection, identifiers: { id: 1 }
+
+ connection.websocket.expects(:transmit)
+ @server.broadcast 'test_room_1', { foo: 'bar' }, coder: DummyEncoder
+ wait_for_async
+ end
+ end
+
+ test "user supplied callbacks are run through the worker pool" do
+ run_in_eventmachine do
+ connection = open_connection
+ receive(connection, command: 'subscribe', channel: UserCallbackChannel.name, identifiers: { id: 1 })
+
+ @server.broadcast 'channel', {}
+ wait_for_async
+ refute Thread.current[:ran_callback], "User callback was not run through the worker pool"
+ end
+ end
+
+ private
+ def subscribe_to(connection, identifiers:)
+ receive connection, command: 'subscribe', identifiers: identifiers
+ end
+
+ def open_connection
+ env = Rack::MockRequest.env_for '/test', 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', 'HTTP_ORIGIN' => 'http://rubyonrails.com'
+
+ Connection.new(@server, env).tap do |connection|
+ connection.process
+ assert connection.websocket.possible?
+
+ wait_for_async
+ assert connection.websocket.alive?
+ end
+ end
+
+ def receive(connection, command:, identifiers:, channel: 'ActionCable::StreamTests::ChatChannel')
+ identifier = JSON.generate(channel: channel, **identifiers)
+ connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier)
+ wait_for_async
+ end
+ end
end
diff --git a/actioncable/test/client/echo_channel.rb b/actioncable/test/client/echo_channel.rb
index 63e35f194a..5a7bac25c5 100644
--- a/actioncable/test/client/echo_channel.rb
+++ b/actioncable/test/client/echo_channel.rb
@@ -3,6 +3,10 @@ class EchoChannel < ActionCable::Channel::Base
stream_from "global"
end
+ def unsubscribed
+ 'Goodbye from EchoChannel!'
+ end
+
def ding(data)
transmit(dong: data['message'])
end
diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb
index 4ade9832e0..5ac453db35 100644
--- a/actioncable/test/client_test.rb
+++ b/actioncable/test/client_test.rb
@@ -8,28 +8,20 @@ require 'faye/websocket'
require 'json'
class ClientTest < ActionCable::TestCase
- WAIT_WHEN_EXPECTING_EVENT = 3
- WAIT_WHEN_NOT_EXPECTING_EVENT = 0.2
+ WAIT_WHEN_EXPECTING_EVENT = 8
+ WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5
def setup
- # TODO: ActionCable requires a *lot* of setup at the moment...
- ::Object.const_set(:ApplicationCable, Module.new)
- ::ApplicationCable.const_set(:Connection, Class.new(ActionCable::Connection::Base))
-
- ::Object.const_set(:Rails, Module.new)
- ::Rails.singleton_class.send(:define_method, :root) { Pathname.new(__dir__) }
-
ActionCable.instance_variable_set(:@server, nil)
server = ActionCable.server
- server.config = ActionCable::Server::Configuration.new
- inner_logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
- server.config.logger = ActionCable::Connection::TaggedLoggerProxy.new(inner_logger, tags: [])
+ server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
server.config.cable = { adapter: 'async' }.with_indifferent_access
+ server.config.use_faye = ENV['FAYE'].present?
# and now the "real" setup for our test:
server.config.disable_request_forgery_protection = true
- server.config.channel_load_paths = [File.expand_path('client', __dir__)]
+ server.config.channel_paths = [ File.expand_path('client/echo_channel.rb', __dir__) ]
Thread.new { EventMachine.run } unless EventMachine.reactor_running?
Thread.pass until EventMachine.reactor_running?
@@ -40,15 +32,6 @@ class ClientTest < ActionCable::TestCase
def teardown
$VERBOSE = @previous_verbose
-
- begin
- ::Object.send(:remove_const, :ApplicationCable)
- rescue NameError
- end
- begin
- ::Object.send(:remove_const, :Rails)
- rescue NameError
- end
end
def with_puma_server(rack_app = ActionCable.server, port = 3099)
@@ -72,7 +55,7 @@ class ClientTest < ActionCable::TestCase
@ws = Faye::WebSocket::Client.new("ws://127.0.0.1:#{port}/")
@messages = Queue.new
@closed = Concurrent::Event.new
- @has_messages = Concurrent::Event.new
+ @has_messages = Concurrent::Semaphore.new(0)
@pings = 0
open = Concurrent::Event.new
@@ -92,12 +75,12 @@ class ClientTest < ActionCable::TestCase
end
@ws.on(:message) do |event|
- hash = JSON.parse(event.data)
- if hash['identifier'] == '_ping'
+ message = JSON.parse(event.data)
+ if message['type'] == 'ping'
@pings += 1
else
- @messages << hash
- @has_messages.set
+ @messages << message
+ @has_messages.release
end
end
@@ -110,8 +93,7 @@ class ClientTest < ActionCable::TestCase
end
def read_message
- @has_messages.wait(WAIT_WHEN_EXPECTING_EVENT) if @messages.empty?
- @has_messages.reset if @messages.size < 2
+ @has_messages.try_acquire(1, WAIT_WHEN_EXPECTING_EVENT)
msg = @messages.pop(true)
raise msg if msg.is_a?(Exception)
@@ -122,9 +104,11 @@ class ClientTest < ActionCable::TestCase
def read_messages(expected_size = 0)
list = []
loop do
- @has_messages.wait(list.size < expected_size ? WAIT_WHEN_EXPECTING_EVENT : WAIT_WHEN_NOT_EXPECTING_EVENT)
- if @has_messages.set?
- list << read_message
+ if @has_messages.try_acquire(1, list.size < expected_size ? WAIT_WHEN_EXPECTING_EVENT : WAIT_WHEN_NOT_EXPECTING_EVENT)
+ msg = @messages.pop(true)
+ raise msg if msg.is_a?(Exception)
+
+ list << msg
else
break
end
@@ -132,8 +116,8 @@ class ClientTest < ActionCable::TestCase
list
end
- def send_message(hash)
- @ws.send(JSON.dump(hash))
+ def send_message(message)
+ @ws.send(JSON.generate(message))
end
def close
@@ -144,8 +128,16 @@ class ClientTest < ActionCable::TestCase
end
@ws.close
+ wait_for_close
+ end
+
+ def wait_for_close
@closed.wait(WAIT_WHEN_EXPECTING_EVENT)
end
+
+ def closed?
+ @closed.set?
+ end
end
def faye_client(port)
@@ -155,9 +147,10 @@ class ClientTest < ActionCable::TestCase
def test_single_client
with_puma_server do |port|
c = faye_client(port)
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "message"=>{"dong"=>"hello"}}, c.read_message)
c.close
end
@@ -171,12 +164,13 @@ class ClientTest < ActionCable::TestCase
barrier_2 = Concurrent::CyclicBarrier.new(clients.size)
clients.map {|c| Concurrent::Future.execute {
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
barrier_1.wait WAIT_WHEN_EXPECTING_EVENT
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'bulk', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'bulk', message: 'hello')
barrier_2.wait WAIT_WHEN_EXPECTING_EVENT
assert_equal clients.size, c.read_messages(clients.size).size
} }.each(&:wait!)
@@ -190,9 +184,10 @@ class ClientTest < ActionCable::TestCase
clients = 100.times.map { faye_client(port) }
clients.map {|c| Concurrent::Future.execute {
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
} }.each(&:wait!)
@@ -203,17 +198,54 @@ class ClientTest < ActionCable::TestCase
def test_disappearing_client
with_puma_server do |port|
c = faye_client(port)
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'delay', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'delay', message: 'hello')
c.close # disappear before write
c = faye_client(port)
- c.send_message command: 'subscribe', identifier: JSON.dump(channel: 'EchoChannel')
+ assert_equal({"type" => "welcome"}, c.read_message) # pop the first welcome message off the stack
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
- c.send_message command: 'message', identifier: JSON.dump(channel: 'EchoChannel'), data: JSON.dump(action: 'ding', message: 'hello')
+ c.send_message command: 'message', identifier: JSON.generate(channel: 'EchoChannel'), data: JSON.generate(action: 'ding', message: 'hello')
assert_equal({"identifier"=>'{"channel":"EchoChannel"}', "message"=>{"dong"=>"hello"}}, c.read_message)
c.close # disappear before read
end
end
+
+ def test_unsubscribe_client
+ with_puma_server do |port|
+ app = ActionCable.server
+ identifier = JSON.generate(channel: 'EchoChannel')
+
+ c = faye_client(port)
+ assert_equal({"type" => "welcome"}, c.read_message)
+ c.send_message command: 'subscribe', identifier: identifier
+ assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
+ assert_equal(1, app.connections.count)
+ assert(app.remote_connections.where(identifier: identifier))
+
+ channel = app.connections.first.subscriptions.send(:subscriptions).first[1]
+ channel.expects(:unsubscribed)
+ c.close
+ sleep 0.1 # Data takes a moment to process
+
+ # All data is removed: No more connection or subscription information!
+ assert_equal(0, app.connections.count)
+ end
+ end
+
+ def test_server_restart
+ with_puma_server do |port|
+ c = faye_client(port)
+ assert_equal({"type" => "welcome"}, c.read_message)
+ c.send_message command: 'subscribe', identifier: JSON.generate(channel: 'EchoChannel')
+ assert_equal({"identifier"=>"{\"channel\":\"EchoChannel\"}", "type"=>"confirm_subscription"}, c.read_message)
+
+ ActionCable.server.restart
+ c.wait_for_close
+ assert c.closed?
+ end
+ end
end
diff --git a/actioncable/test/connection/authorization_test.rb b/actioncable/test/connection/authorization_test.rb
index 87d0e79ef3..a0506cb9c0 100644
--- a/actioncable/test/connection/authorization_test.rb
+++ b/actioncable/test/connection/authorization_test.rb
@@ -20,7 +20,7 @@ class ActionCable::Connection::AuthorizationTest < ActionCable::TestCase
server.config.allowed_request_origins = %w( http://rubyonrails.com )
env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket',
- 'HTTP_ORIGIN' => 'http://rubyonrails.com'
+ 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com'
connection = Connection.new(server, env)
connection.websocket.expects(:close)
diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb
index e2b017a9a1..d7e1041e68 100644
--- a/actioncable/test/connection/base_test.rb
+++ b/actioncable/test/connection/base_test.rb
@@ -1,5 +1,6 @@
require 'test_helper'
require 'stubs/test_server'
+require 'active_support/core_ext/object/json'
class ActionCable::Connection::BaseTest < ActionCable::TestCase
class Connection < ActionCable::Connection::Base
@@ -56,7 +57,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
run_in_eventmachine do
connection = open_connection
- connection.websocket.expects(:transmit).with(regexp_matches(/\_ping/))
+ connection.websocket.expects(:transmit).with({ type: "welcome" }.to_json)
connection.message_buffer.expects(:process!)
connection.process
@@ -73,7 +74,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
connection.process
# Setup the connection
- Concurrent::TimerTask.stubs(:new).returns(true)
+ connection.server.stubs(:timer).returns(true)
connection.send :handle_open
assert connection.connected
@@ -108,10 +109,30 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase
end
end
+ test "rejecting a connection causes a 404" do
+ run_in_eventmachine do
+ class CallMeMaybe
+ def call(*)
+ raise 'Do not call me!'
+ end
+ end
+
+ env = Rack::MockRequest.env_for(
+ "/test",
+ { 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket',
+ 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.org', 'rack.hijack' => CallMeMaybe.new }
+ )
+
+ connection = ActionCable::Connection::Base.new(@server, env)
+ response = connection.process
+ assert_equal 404, response[0]
+ end
+ end
+
private
def open_connection
env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket',
- 'HTTP_ORIGIN' => 'http://rubyonrails.com'
+ 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com'
Connection.new(@server, env)
end
diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb
new file mode 100644
index 0000000000..4af071b4da
--- /dev/null
+++ b/actioncable/test/connection/client_socket_test.rb
@@ -0,0 +1,64 @@
+require 'test_helper'
+require 'stubs/test_server'
+
+class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ attr_reader :connected, :websocket, :errors
+
+ def initialize(*)
+ super
+ @errors = []
+ end
+
+ def connect
+ @connected = true
+ end
+
+ def disconnect
+ @connected = false
+ end
+
+ def send_async(method, *args)
+ send method, *args
+ end
+
+ def on_error(message)
+ @errors << message
+ end
+ end
+
+ setup do
+ @server = TestServer.new
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ end
+
+ test 'delegate socket errors to on_error handler' do
+ skip if ENV['FAYE'].present?
+
+ run_in_eventmachine do
+ connection = open_connection
+
+ # Internal hax = :(
+ client = connection.websocket.send(:websocket)
+ client.instance_variable_get('@stream').expects(:write).raises('foo')
+ client.expects(:client_gone).never
+
+ client.write('boo')
+ assert_equal %w[ foo ], connection.errors
+ end
+ end
+
+ private
+ def open_connection
+ env = Rack::MockRequest.env_for '/test',
+ 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket',
+ 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com'
+ env['rack.hijack'] = -> { env['rack.hijack_io'] = StringIO.new }
+
+ Connection.new(@server, env).tap do |connection|
+ connection.process
+ connection.send :handle_open
+ assert connection.connected
+ end
+ end
+end
diff --git a/actioncable/test/connection/cross_site_forgery_test.rb b/actioncable/test/connection/cross_site_forgery_test.rb
index a29f65fb97..2d516b0533 100644
--- a/actioncable/test/connection/cross_site_forgery_test.rb
+++ b/actioncable/test/connection/cross_site_forgery_test.rb
@@ -76,6 +76,6 @@ class ActionCable::Connection::CrossSiteForgeryTest < ActionCable::TestCase
def env_for_origin(origin)
Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket', 'SERVER_NAME' => HOST,
- 'HTTP_ORIGIN' => origin
+ 'HTTP_HOST' => HOST, 'HTTP_ORIGIN' => origin
end
end
diff --git a/actioncable/test/connection/identifier_test.rb b/actioncable/test/connection/identifier_test.rb
index 1019ad541e..b48d9af809 100644
--- a/actioncable/test/connection/identifier_test.rb
+++ b/actioncable/test/connection/identifier_test.rb
@@ -40,8 +40,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
open_connection_with_stubbed_pubsub
@connection.websocket.expects(:close)
- message = ActiveSupport::JSON.encode('type' => 'disconnect')
- @connection.process_internal_message message
+ @connection.process_internal_message 'type' => 'disconnect'
end
end
@@ -50,8 +49,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
open_connection_with_stubbed_pubsub
@connection.websocket.expects(:close).never
- message = ActiveSupport::JSON.encode('type' => 'unknown')
- @connection.process_internal_message message
+ @connection.process_internal_message 'type' => 'unknown'
end
end
@@ -64,7 +62,7 @@ class ActionCable::Connection::IdentifierTest < ActionCable::TestCase
end
def open_connection(server:)
- env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
@connection = Connection.new(server, env)
@connection.process
diff --git a/actioncable/test/connection/multiple_identifiers_test.rb b/actioncable/test/connection/multiple_identifiers_test.rb
index e9bb4e6d7f..484e73bb30 100644
--- a/actioncable/test/connection/multiple_identifiers_test.rb
+++ b/actioncable/test/connection/multiple_identifiers_test.rb
@@ -28,7 +28,7 @@ class ActionCable::Connection::MultipleIdentifiersTest < ActionCable::TestCase
end
def open_connection(server:)
- env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
@connection = Connection.new(server, env)
@connection.process
diff --git a/actioncable/test/connection/stream_test.rb b/actioncable/test/connection/stream_test.rb
new file mode 100644
index 0000000000..a7a61d8d6f
--- /dev/null
+++ b/actioncable/test/connection/stream_test.rb
@@ -0,0 +1,66 @@
+require 'test_helper'
+require 'stubs/test_server'
+
+class ActionCable::Connection::StreamTest < ActionCable::TestCase
+ class Connection < ActionCable::Connection::Base
+ attr_reader :connected, :websocket, :errors
+
+ def initialize(*)
+ super
+ @errors = []
+ end
+
+ def connect
+ @connected = true
+ end
+
+ def disconnect
+ @connected = false
+ end
+
+ def send_async(method, *args)
+ send method, *args
+ end
+
+ def on_error(message)
+ @errors << message
+ end
+ end
+
+ setup do
+ @server = TestServer.new
+ @server.config.allowed_request_origins = %w( http://rubyonrails.com )
+ end
+
+ [ EOFError, Errno::ECONNRESET ].each do |closed_exception|
+ test "closes socket on #{closed_exception}" do
+ skip if ENV['FAYE'].present?
+
+ run_in_eventmachine do
+ connection = open_connection
+
+ # Internal hax = :(
+ client = connection.websocket.send(:websocket)
+ client.instance_variable_get('@stream').instance_variable_get('@rack_hijack_io').expects(:write).raises(closed_exception, 'foo')
+ client.expects(:client_gone)
+
+ client.write('boo')
+ assert_equal [], connection.errors
+ end
+ end
+ end
+
+ private
+ def open_connection
+ env = Rack::MockRequest.env_for '/test',
+ 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket',
+ 'HTTP_HOST' => 'localhost', 'HTTP_ORIGIN' => 'http://rubyonrails.com'
+ env['rack.hijack'] = -> { env['rack.hijack_io'] = StringIO.new }
+
+ Connection.new(@server, env).tap do |connection|
+ connection.process
+ connection.send :handle_open
+ assert connection.connected
+ end
+ end
+end
diff --git a/actioncable/test/connection/string_identifier_test.rb b/actioncable/test/connection/string_identifier_test.rb
index 9d0bda83ef..eca0c31060 100644
--- a/actioncable/test/connection/string_identifier_test.rb
+++ b/actioncable/test/connection/string_identifier_test.rb
@@ -30,7 +30,7 @@ class ActionCable::Connection::StringIdentifierTest < ActionCable::TestCase
end
def open_connection
- env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
@connection = Connection.new(@server, env)
@connection.process
diff --git a/actioncable/test/connection/subscriptions_test.rb b/actioncable/test/connection/subscriptions_test.rb
index 62e41484fe..53e8547245 100644
--- a/actioncable/test/connection/subscriptions_test.rb
+++ b/actioncable/test/connection/subscriptions_test.rb
@@ -82,7 +82,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
end
end
- test "unsubscrib from all" do
+ test "unsubscribe from all" do
run_in_eventmachine do
setup_connection
@@ -107,7 +107,7 @@ class ActionCable::Connection::SubscriptionsTest < ActionCable::TestCase
end
def setup_connection
- env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
+ env = Rack::MockRequest.env_for "/test", 'HTTP_HOST' => 'localhost', 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket'
@connection = Connection.new(@server, env)
@subscriptions = ActionCable::Connection::Subscriptions.new(@connection)
diff --git a/actioncable/test/server/broadcasting_test.rb b/actioncable/test/server/broadcasting_test.rb
new file mode 100644
index 0000000000..3b4a7eaf90
--- /dev/null
+++ b/actioncable/test/server/broadcasting_test.rb
@@ -0,0 +1,15 @@
+require "test_helper"
+
+class BroadcastingTest < ActiveSupport::TestCase
+ class TestServer
+ include ActionCable::Server::Broadcasting
+ end
+
+ test "fetching a broadcaster converts the broadcasting queue to a string" do
+ broadcasting = :test_queue
+ server = TestServer.new
+ broadcaster = server.broadcaster_for(broadcasting)
+
+ assert_equal "test_queue", broadcaster.broadcasting
+ end
+end
diff --git a/actioncable/test/stubs/test_connection.rb b/actioncable/test/stubs/test_connection.rb
index da98201900..885450dda6 100644
--- a/actioncable/test/stubs/test_connection.rb
+++ b/actioncable/test/stubs/test_connection.rb
@@ -1,25 +1,33 @@
require 'stubs/user'
class TestConnection
- attr_reader :identifiers, :logger, :current_user, :transmissions
+ attr_reader :identifiers, :logger, :current_user, :server, :transmissions
- def initialize(user = User.new("lifo"))
+ delegate :pubsub, to: :server
+
+ def initialize(user = User.new("lifo"), coder: ActiveSupport::JSON, subscription_adapter: SuccessAdapter)
+ @coder = coder
@identifiers = [ :current_user ]
@current_user = user
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
+ @server = TestServer.new(subscription_adapter: subscription_adapter)
@transmissions = []
end
- def pubsub
- SuccessAdapter.new(TestServer.new)
+ def transmit(cable_message)
+ @transmissions << encode(cable_message)
end
- def transmit(data)
- @transmissions << data
+ def last_transmission
+ decode @transmissions.last if @transmissions.any?
end
- def last_transmission
- @transmissions.last
+ def decode(websocket_message)
+ @coder.decode websocket_message
+ end
+
+ def encode(cable_message)
+ @coder.encode cable_message
end
end
diff --git a/actioncable/test/stubs/test_server.rb b/actioncable/test/stubs/test_server.rb
index 56d132b30a..b86f422a13 100644
--- a/actioncable/test/stubs/test_server.rb
+++ b/actioncable/test/stubs/test_server.rb
@@ -2,19 +2,37 @@ require 'ostruct'
class TestServer
include ActionCable::Server::Connections
+ include ActionCable::Server::Broadcasting
- attr_reader :logger, :config
+ attr_reader :logger, :config, :mutex
- def initialize
+ def initialize(subscription_adapter: SuccessAdapter)
@logger = ActiveSupport::TaggedLogging.new ActiveSupport::Logger.new(StringIO.new)
- @config = OpenStruct.new(log_tags: [], subscription_adapter: SuccessAdapter)
+
+ @config = OpenStruct.new(log_tags: [], subscription_adapter: subscription_adapter)
+ @config.use_faye = ENV['FAYE'].present?
+ @config.client_socket_class = if @config.use_faye
+ ActionCable::Connection::FayeClientSocket
+ else
+ ActionCable::Connection::ClientSocket
+ end
+
+ @mutex = Monitor.new
end
def pubsub
- @config.subscription_adapter.new(self)
+ @pubsub ||= @config.subscription_adapter.new(self)
+ end
+
+ def event_loop
+ @event_loop ||= if @config.use_faye
+ ActionCable::Connection::FayeEventLoop.new
+ else
+ ActionCable::Connection::StreamEventLoop.new
+ end
end
- def stream_event_loop
- @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new
+ def worker_pool
+ @worker_pool ||= ActionCable::Server::Worker.new(max_size: 5)
end
end
diff --git a/actioncable/test/subscription_adapter/base_test.rb b/actioncable/test/subscription_adapter/base_test.rb
index 7a7ae131e6..256dce673f 100644
--- a/actioncable/test/subscription_adapter/base_test.rb
+++ b/actioncable/test/subscription_adapter/base_test.rb
@@ -43,7 +43,7 @@ class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase
assert_respond_to(SuccessAdapter.new(@server), :broadcast)
- assert_nothing_raised NotImplementedError do
+ assert_nothing_raised do
broadcast
end
end
@@ -55,7 +55,7 @@ class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase
assert_respond_to(SuccessAdapter.new(@server), :subscribe)
- assert_nothing_raised NotImplementedError do
+ assert_nothing_raised do
subscribe
end
end
@@ -66,7 +66,7 @@ class ActionCable::SubscriptionAdapter::BaseTest < ActionCable::TestCase
assert_respond_to(SuccessAdapter.new(@server), :unsubscribe)
- assert_nothing_raised NotImplementedError do
+ assert_nothing_raised do
unsubscribe
end
end
diff --git a/actioncable/test/subscription_adapter/common.rb b/actioncable/test/subscription_adapter/common.rb
index 361858784e..285c690df0 100644
--- a/actioncable/test/subscription_adapter/common.rb
+++ b/actioncable/test/subscription_adapter/common.rb
@@ -9,21 +9,9 @@ module CommonSubscriptionAdapterTest
WAIT_WHEN_NOT_EXPECTING_EVENT = 0.2
def setup
- # TODO: ActionCable requires a *lot* of setup at the moment...
- ::Object.const_set(:ApplicationCable, Module.new)
- ::ApplicationCable.const_set(:Connection, Class.new(ActionCable::Connection::Base))
-
- ::Object.const_set(:Rails, Module.new)
- ::Rails.singleton_class.send(:define_method, :root) { Pathname.new(__dir__) }
-
server = ActionCable::Server::Base.new
- server.config = ActionCable::Server::Configuration.new
- inner_logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
- server.config.logger = ActionCable::Connection::TaggedLoggerProxy.new(inner_logger, tags: [])
-
-
- # and now the "real" setup for our test:
server.config.cable = cable_config.with_indifferent_access
+ server.config.use_faye = ENV['FAYE'].present?
adapter_klass = server.config.pubsub_adapter
@@ -32,17 +20,7 @@ module CommonSubscriptionAdapterTest
end
def teardown
- @tx_adapter.shutdown if @tx_adapter && @tx_adapter != @rx_adapter
- @rx_adapter.shutdown if @rx_adapter
-
- begin
- ::Object.send(:remove_const, :ApplicationCable)
- rescue NameError
- end
- begin
- ::Object.send(:remove_const, :Rails)
- rescue NameError
- end
+ [@rx_adapter, @tx_adapter].uniq.each(&:shutdown)
end
diff --git a/actioncable/test/subscription_adapter/evented_redis_test.rb b/actioncable/test/subscription_adapter/evented_redis_test.rb
index 70333e51bd..6d20e6ed78 100644
--- a/actioncable/test/subscription_adapter/evented_redis_test.rb
+++ b/actioncable/test/subscription_adapter/evented_redis_test.rb
@@ -4,6 +4,17 @@ require_relative './common'
class EventedRedisAdapterTest < ActionCable::TestCase
include CommonSubscriptionAdapterTest
+ def setup
+ super
+
+ # em-hiredis is warning-rich
+ @previous_verbose, $VERBOSE = $VERBOSE, nil
+ end
+
+ def teardown
+ $VERBOSE = @previous_verbose
+ end
+
def cable_config
{ adapter: 'evented_redis', url: 'redis://127.0.0.1:6379/12' }
end
diff --git a/actioncable/test/subscription_adapter/postgresql_test.rb b/actioncable/test/subscription_adapter/postgresql_test.rb
index 64c632b0cd..214352a0b2 100644
--- a/actioncable/test/subscription_adapter/postgresql_test.rb
+++ b/actioncable/test/subscription_adapter/postgresql_test.rb
@@ -15,8 +15,16 @@ class PostgresqlAdapterTest < ActionCable::TestCase
local_config = ARTest.config['arunit']
database_config.update local_config if local_config
end
+
ActiveRecord::Base.establish_connection database_config
+ begin
+ ActiveRecord::Base.connection
+ rescue
+ @rx_adapter = @tx_adapter = nil
+ skip "Couldn't connect to PostgreSQL: #{database_config.inspect}"
+ end
+
super
end
diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb
index 8ddbd4e764..de1ee96770 100644
--- a/actioncable/test/test_helper.rb
+++ b/actioncable/test/test_helper.rb
@@ -1,19 +1,53 @@
-require File.expand_path('../../../load_paths', __FILE__)
-
require 'action_cable'
require 'active_support/testing/autorun'
-
require 'puma'
-
require 'mocha/setup'
-
require 'rack/mock'
+begin
+ require 'byebug'
+rescue LoadError
+end
+
# Require all the stubs and models
Dir[File.dirname(__FILE__) + '/stubs/*.rb'].each {|file| require file }
-class ActionCable::TestCase < ActiveSupport::TestCase
+if ENV['FAYE'].present?
+ require 'faye/websocket'
+ class << Faye::WebSocket
+ remove_method :ensure_reactor_running
+
+ # We don't want Faye to start the EM reactor in tests because it makes testing much harder.
+ # We want to be able to start and stop EM loop in tests to make things simpler.
+ def ensure_reactor_running
+ # no-op
+ end
+ end
+end
+
+module EventMachineConcurrencyHelpers
+ def wait_for_async
+ EM.run_deferred_callbacks
+ end
+
+ def run_in_eventmachine
+ failure = nil
+ EM.run do
+ begin
+ yield
+ rescue => ex
+ failure = ex
+ ensure
+ wait_for_async
+ EM.stop if EM.reactor_running?
+ end
+ end
+ raise failure if failure
+ end
+end
+
+module ConcurrentRubyConcurrencyHelpers
def wait_for_async
e = Concurrent.global_io_executor
until e.completed_task_count == e.scheduled_task_count
@@ -26,3 +60,11 @@ class ActionCable::TestCase < ActiveSupport::TestCase
wait_for_async
end
end
+
+class ActionCable::TestCase < ActiveSupport::TestCase
+ if ENV['FAYE'].present?
+ include EventMachineConcurrencyHelpers
+ else
+ include ConcurrentRubyConcurrencyHelpers
+ end
+end
diff --git a/actioncable/test/worker_test.rb b/actioncable/test/worker_test.rb
index 4a699cde27..7016da3493 100644
--- a/actioncable/test/worker_test.rb
+++ b/actioncable/test/worker_test.rb
@@ -17,7 +17,9 @@ class WorkerTest < ActiveSupport::TestCase
end
def logger
- ActionCable.server.logger
+ # Impersonating a connection requires a TaggedLoggerProxy'ied logger.
+ inner_logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN }
+ ActionCable::Connection::TaggedLoggerProxy.new(inner_logger, tags: [])
end
end
@@ -39,14 +41,4 @@ class WorkerTest < ActiveSupport::TestCase
@worker.invoke @receiver, :process, "Hello"
assert_equal [ :process, "Hello" ], @receiver.last_action
end
-
- test "running periodic timers with a proc" do
- @worker.run_periodic_timer @receiver, @receiver.method(:run)
- assert_equal :run, @receiver.last_action
- end
-
- test "running periodic timers with a method" do
- @worker.run_periodic_timer @receiver, :run
- assert_equal :run, @receiver.last_action
- end
end
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 22e9bd12a1..76d99f31c7 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,3 +1,39 @@
+* Disallow calling `#deliver_later` after making local modifications to
+ the message which would be lost when the delivery job is enqueued.
+
+ Prevents a common, hard-to-find bug like:
+
+ message = Notifier.welcome(user, foo)
+ message.message_id = my_generated_message_id
+ message.deliver_later
+
+ The message_id is silently lost! *Only the mailer arguments are passed
+ to the delivery job.*
+
+ This raises an exception now. Make modifications to the message within
+ the mailer method instead, or use a custom Active Job to manage delivery
+ instead of using #deliver_later.
+
+ *Jeremy Daer*
+
+* Removes `-t` from default Sendmail arguments to match the underlying
+ `Mail::Sendmail` setting.
+
+ *Clayton Liggitt*
+
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* Add support for fragment caching in Action Mailer views.
+
+ *Stan Lo*
+
+* Reset `ActionMailer::Base.deliveries` after every test in
+ `ActionDispatch::IntegrationTest`.
+
+ *Yves Senn*
+
+
## Rails 5.0.0.beta2 (February 01, 2016) ##
* No changes.
@@ -5,8 +41,7 @@
## Rails 5.0.0.beta1 (December 18, 2015) ##
-* `config.force_ssl = true` will set
- `config.action_mailer.default_url_options = { protocol: 'https' }`.
+* `config.action_mailer.default_url_options[:protocol]` is now set to `https` if `config.force_ssl` is set to `true`.
*Andrew Kampjes*
@@ -15,7 +50,7 @@
*Chris McGrath*
-* `assert_emails` in block form use the given number as expected value.
+* `assert_emails` in block form, uses the given number as expected value.
This makes the error message much easier to understand.
*Yuji Yaginuma*
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index bb3cb1be45..4bb7842297 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -86,7 +86,7 @@ module ActionMailer
# Like Action Controller, each mailer class has a corresponding view directory in which each
# method of the class looks for a template with its name.
#
- # To define a template to be used with a mailing, create an <tt>.erb</tt> file with the same
+ # To define a template to be used with a mailer, create an <tt>.erb</tt> file with the same
# name as the method in your mailer model. For example, in the mailer defined above, the template at
# <tt>app/views/notifier_mailer/welcome.text.erb</tt> would be used to generate the email.
#
@@ -144,7 +144,7 @@ module ActionMailer
# mail.deliver_now # generates and sends the email now
#
# The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a delegate that will call
- # your method to generate the mail. If you want direct access to delegator, or <tt>Mail::Message</tt>,
+ # your method to generate the mail. If you want direct access to the delegator, or <tt>Mail::Message</tt>,
# you can call the <tt>message</tt> method on the <tt>ActionMailer::MessageDelivery</tt> object.
#
# NotifierMailer.welcome(User.first).message # => a Mail::Message object
@@ -163,7 +163,7 @@ module ActionMailer
#
# Multipart messages can also be used implicitly because Action Mailer will automatically detect and use
# multipart templates, where each template is named after the name of the action, followed by the content
- # type. Each such detected template will be added as a separate part to the message.
+ # type. Each such detected template will be added to the message, as a separate part.
#
# For example, if the following templates exist:
# * signup_notification.text.erb
@@ -288,8 +288,8 @@ module ActionMailer
# end
#
# Note that the proc is evaluated right at the start of the mail message generation, so if you
- # set something in the default using a proc, and then set the same thing inside of your
- # mailer method, it will get over written by the mailer method.
+ # set something in the default hash using a proc, and then set the same thing inside of your
+ # mailer method, it will get overwritten by the mailer method.
#
# It is also possible to set these default options that will be used in all mailers through
# the <tt>default_options=</tt> configuration in <tt>config/application.rb</tt>:
@@ -430,6 +430,7 @@ module ActionMailer
include AbstractController::Translation
include AbstractController::AssetPaths
include AbstractController::Callbacks
+ include AbstractController::Caching
include ActionView::Layouts
@@ -664,7 +665,7 @@ module ActionMailer
#
# You can also specify overrides if you want by passing a hash instead of a string:
#
- # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
+ # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
# content: File.read('/path/to/filename.jpg')}
#
# If you want to use encoding other than Base64 then you will need to pass encoding
@@ -672,7 +673,7 @@ module ActionMailer
# data:
#
# file_content = SpecialEncode(File.read('/path/to/filename.jpg'))
- # mail.attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
+ # mail.attachments['filename.jpg'] = {mime_type: 'application/gzip',
# encoding: 'SpecialEncoding',
# content: file_content }
#
@@ -947,6 +948,18 @@ module ActionMailer
container.add_part(part)
end
+ # This and #instrument_name is for caching instrument
+ def instrument_payload(key)
+ {
+ mailer: mailer_name,
+ key: key
+ }
+ end
+
+ def instrument_name
+ "action_mailer"
+ end
+
ActiveSupport.run_load_hooks(:action_mailer, self)
end
end
diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb
index 4758b55a2a..571c8e7d2a 100644
--- a/actionmailer/lib/action_mailer/delivery_methods.rb
+++ b/actionmailer/lib/action_mailer/delivery_methods.rb
@@ -36,7 +36,7 @@ module ActionMailer
add_delivery_method :sendmail, Mail::Sendmail,
location: '/usr/sbin/sendmail',
- arguments: '-i -t'
+ arguments: '-i'
add_delivery_method :test, Mail::TestMailer
end
diff --git a/actionmailer/lib/action_mailer/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb
index a1ee5fb238..cbe5fc3e64 100644
--- a/actionmailer/lib/action_mailer/gem_version.rb
+++ b/actionmailer/lib/action_mailer/gem_version.rb
@@ -8,7 +8,7 @@ module ActionMailer
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
index 6d02b39225..419d6c7b93 100644
--- a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
+++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb
@@ -3,7 +3,7 @@ require 'base64'
module ActionMailer
# Implements a mailer preview interceptor that converts image tag src attributes
# that use inline cid: style urls to data: style urls so that they are visible
- # when previewing a HTML email in a web browser.
+ # when previewing an HTML email in a web browser.
#
# This interceptor is enabled by default. To disable it, delete it from the
# <tt>ActionMailer::Base.preview_interceptors</tt> array:
diff --git a/actionmailer/lib/action_mailer/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb
index 5fcb5a0c88..d638057d72 100644
--- a/actionmailer/lib/action_mailer/message_delivery.rb
+++ b/actionmailer/lib/action_mailer/message_delivery.rb
@@ -18,6 +18,7 @@ module ActionMailer
@mailer = mailer
@mail_method = mail_method
@args = args
+ @obj = nil
end
def __getobj__ #:nodoc:
@@ -91,8 +92,19 @@ module ActionMailer
private
def enqueue_delivery(delivery_method, options={})
- args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args
- ActionMailer::DeliveryJob.set(options).perform_later(*args)
+ if @obj
+ raise "You've accessed the message before asking to deliver it " \
+ "later, so you may have made local changes that would be " \
+ "silently lost if we enqueued a job to deliver it. Why? Only " \
+ "the mailer method *arguments* are passed with the delivery job! " \
+ "Do not access the message in any way if you mean to deliver it " \
+ "later. Workarounds: 1. don't touch the message before calling " \
+ "#deliver_later, 2. only touch the message *within your mailer " \
+ "method*, or 3. use a custom Active Job instead of #deliver_later."
+ else
+ args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args
+ ActionMailer::DeliveryJob.set(options).perform_later(*args)
+ end
end
end
end
diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index fa707021c7..a727ed38e9 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -25,6 +25,7 @@ module ActionMailer
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
options.show_previews = Rails.env.development? if options.show_previews.nil?
+ options.cache_store ||= Rails.cache
if options.show_previews
options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
@@ -44,14 +45,9 @@ module ActionMailer
register_observers(options.delete(:observers))
options.each { |k,v| send("#{k}=", v) }
-
- if options.show_previews
- app.routes.prepend do
- get '/rails/mailers' => "rails/mailers#index"
- get '/rails/mailers/*path' => "rails/mailers#preview"
- end
- end
end
+
+ ActiveSupport.on_load(:action_dispatch_integration_test) { include ActionMailer::TestCase::ClearTestDeliveries }
end
initializer "action_mailer.compile_config_methods" do
@@ -60,9 +56,18 @@ module ActionMailer
end
end
- config.after_initialize do
- if ActionMailer::Base.preview_path
- ActiveSupport::Dependencies.autoload_paths << ActionMailer::Base.preview_path
+ config.after_initialize do |app|
+ options = app.config.action_mailer
+
+ 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
+ end
end
end
end
diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb
index 0aa15e31ba..d83719e57d 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -11,6 +11,21 @@ module ActionMailer
end
class TestCase < ActiveSupport::TestCase
+ module ClearTestDeliveries
+ extend ActiveSupport::Concern
+
+ included do
+ teardown :clear_test_deliviers
+ end
+
+ private
+ def clear_test_deliviers
+ if ActionMailer::Base.delivery_method == :test
+ ActionMailer::Base.deliveries.clear
+ end
+ end
+ end
+
module Behavior
extend ActiveSupport::Concern
@@ -66,7 +81,6 @@ module ActionMailer
def restore_test_deliveries # :nodoc:
restore_delivery_method
ActionMailer::Base.perform_deliveries = @old_perform_deliveries
- ActionMailer::Base.deliveries.clear
end
def set_delivery_method(method) # :nodoc:
@@ -100,5 +114,6 @@ module ActionMailer
end
include Behavior
+ include ClearTestDeliveries
end
end
diff --git a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
index 5a5c9d32bb..01bdfb0685 100644
--- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
+++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
@@ -9,13 +9,28 @@ module Rails
def create_mailer_file
template "mailer.rb", File.join('app/mailers', class_path, "#{file_name}_mailer.rb")
+
+ in_root do
+ if self.behavior == :invoke && !File.exist?(application_mailer_file_name)
+ template 'application_mailer.rb', application_mailer_file_name
+ end
+ end
end
hook_for :template_engine, :test_framework
protected
def file_name
- @_file_name ||= super.gsub(/\_mailer/i, '')
+ @_file_name ||= super.gsub(/_mailer/i, '')
+ end
+
+ private
+ def application_mailer_file_name
+ @_application_mailer_file_name ||= if mountable_engine?
+ "app/mailers/#{namespaced_path}/application_mailer.rb"
+ else
+ "app/mailers/application_mailer.rb"
+ end
end
end
end
diff --git a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb
new file mode 100644
index 0000000000..00fb9bd48f
--- /dev/null
+++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb
@@ -0,0 +1,6 @@
+<% module_namespacing do -%>
+class ApplicationMailer < ActionMailer::Base
+ default from: 'from@example.com'
+ layout 'mailer'
+end
+<% end %>
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 285b2cfcb5..8d740ac863 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -1,4 +1,3 @@
-require File.expand_path('../../../load_paths', __FILE__)
require 'active_support/core_ext/kernel/reporting'
# These are the normal settings that will be set up by Railties
diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb
new file mode 100644
index 0000000000..b4344eb167
--- /dev/null
+++ b/actionmailer/test/caching_test.rb
@@ -0,0 +1,229 @@
+require 'fileutils'
+require 'abstract_unit'
+require 'mailers/base_mailer'
+require 'mailers/caching_mailer'
+
+CACHE_DIR = 'test_cache'
+# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
+FILE_STORE_PATH = File.join(File.dirname(__FILE__), '/../temp/', CACHE_DIR)
+
+class FragmentCachingMailer < ActionMailer::Base
+ abstract!
+
+ def some_action; end
+end
+
+class BaseCachingTest < ActiveSupport::TestCase
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @mailer = FragmentCachingMailer.new
+ @mailer.perform_caching = true
+ @mailer.cache_store = @store
+ end
+
+ def test_fragment_cache_key
+ assert_equal 'views/what a key', @mailer.fragment_cache_key('what a key')
+ end
+end
+
+class FragmentCachingTest < BaseCachingTest
+ def test_read_fragment_with_caching_enabled
+ @store.write('views/name', 'value')
+ assert_equal 'value', @mailer.read_fragment('name')
+ end
+
+ def test_read_fragment_with_caching_disabled
+ @mailer.perform_caching = false
+ @store.write('views/name', 'value')
+ assert_nil @mailer.read_fragment('name')
+ end
+
+ def test_fragment_exist_with_caching_enabled
+ @store.write('views/name', 'value')
+ assert @mailer.fragment_exist?('name')
+ assert !@mailer.fragment_exist?('other_name')
+ end
+
+ def test_fragment_exist_with_caching_disabled
+ @mailer.perform_caching = false
+ @store.write('views/name', 'value')
+ assert !@mailer.fragment_exist?('name')
+ assert !@mailer.fragment_exist?('other_name')
+ end
+
+ def test_write_fragment_with_caching_enabled
+ assert_nil @store.read('views/name')
+ assert_equal 'value', @mailer.write_fragment('name', 'value')
+ assert_equal 'value', @store.read('views/name')
+ end
+
+ def test_write_fragment_with_caching_disabled
+ assert_nil @store.read('views/name')
+ @mailer.perform_caching = false
+ assert_equal 'value', @mailer.write_fragment('name', 'value')
+ assert_nil @store.read('views/name')
+ end
+
+ def test_expire_fragment_with_simple_key
+ @store.write('views/name', 'value')
+ @mailer.expire_fragment 'name'
+ assert_nil @store.read('views/name')
+ end
+
+ def test_expire_fragment_with_regexp
+ @store.write('views/name', 'value')
+ @store.write('views/another_name', 'another_value')
+ @store.write('views/primalgrasp', 'will not expire ;-)')
+
+ @mailer.expire_fragment(/name/)
+
+ assert_nil @store.read('views/name')
+ assert_nil @store.read('views/another_name')
+ assert_equal 'will not expire ;-)', @store.read('views/primalgrasp')
+ end
+
+ def test_fragment_for
+ @store.write('views/expensive', 'fragment content')
+ fragment_computed = false
+
+ view_context = @mailer.view_context
+
+ buffer = 'generated till now -> '.html_safe
+ buffer << view_context.send(:fragment_for, 'expensive') { fragment_computed = true }
+
+ assert !fragment_computed
+ assert_equal 'generated till now -> fragment content', buffer
+ end
+
+ def test_html_safety
+ assert_nil @store.read('views/name')
+ content = 'value'.html_safe
+ assert_equal content, @mailer.write_fragment('name', content)
+
+ cached = @store.read('views/name')
+ assert_equal content, cached
+ assert_equal String, cached.class
+
+ html_safe = @mailer.read_fragment('name')
+ assert_equal content, html_safe
+ assert html_safe.html_safe?
+ end
+end
+
+class FunctionalFragmentCachingTest < BaseCachingTest
+ def setup
+ super
+ @store = ActiveSupport::Cache::MemoryStore.new
+ @mailer = CachingMailer.new
+ @mailer.perform_caching = true
+ @mailer.cache_store = @store
+ end
+
+ def test_fragment_caching
+ email = @mailer.fragment_cache
+ expected_body = "\"Welcome\""
+
+ assert_match expected_body, email.body.encoded
+ assert_match "\"Welcome\"",
+ @store.read("views/caching/#{template_digest("caching_mailer/fragment_cache")}")
+ end
+
+ def test_fragment_caching_in_partials
+ email = @mailer.fragment_cache_in_partials
+ assert_match(/Old fragment caching in a partial/, email.body.encoded)
+
+ assert_match("Old fragment caching in a partial",
+ @store.read("views/caching/#{template_digest("caching_mailer/_partial")}"))
+ end
+
+ def test_skip_fragment_cache_digesting
+ email = @mailer.skip_fragment_cache_digesting
+ expected_body = "No Digest"
+
+ assert_match expected_body, email.body.encoded
+ assert_match expected_body, @store.read("views/no_digest")
+ end
+
+ private
+
+ def template_digest(name)
+ ActionView::Digestor.digest(name: name, finder: @mailer.lookup_context)
+ end
+end
+
+class CacheHelperOutputBufferTest < BaseCachingTest
+
+ class MockController
+ def read_fragment(name, options)
+ return false
+ end
+
+ def write_fragment(name, fragment, options)
+ fragment
+ end
+ end
+
+ def setup
+ super
+ end
+
+ def test_output_buffer
+ output_buffer = ActionView::OutputBuffer.new
+ controller = MockController.new
+ cache_helper = Class.new do
+ def self.controller; end;
+ def self.output_buffer; end;
+ def self.output_buffer=; end;
+ end
+ cache_helper.extend(ActionView::Helpers::CacheHelper)
+
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ end
+ end
+ end
+ end
+ end
+
+ def test_safe_buffer
+ output_buffer = ActiveSupport::SafeBuffer.new
+ controller = MockController.new
+ cache_helper = Class.new do
+ def self.controller; end;
+ def self.output_buffer; end;
+ def self.output_buffer=; end;
+ end
+ cache_helper.extend(ActionView::Helpers::CacheHelper)
+
+ cache_helper.stub :controller, controller do
+ cache_helper.stub :output_buffer, output_buffer do
+ assert_called_with cache_helper, :output_buffer=, [output_buffer.class.new(output_buffer)] do
+ assert_nothing_raised do
+ cache_helper.send :fragment_for, 'Test fragment name', 'Test fragment', &Proc.new{ nil }
+ end
+ end
+ end
+ end
+ end
+end
+
+class ViewCacheDependencyTest < BaseCachingTest
+ class NoDependenciesMailer < ActionMailer::Base
+ end
+ class HasDependenciesMailer < ActionMailer::Base
+ view_cache_dependency { "trombone" }
+ view_cache_dependency { "flute" }
+ end
+
+ def test_view_cache_dependencies_are_empty_by_default
+ assert NoDependenciesMailer.new.view_cache_dependencies.empty?
+ end
+
+ def test_view_cache_dependencies_are_listed_in_declaration_order
+ assert_equal %w(trombone flute), HasDependenciesMailer.new.view_cache_dependencies
+ end
+end
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 2786fe0d07..bcbd036f26 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -39,7 +39,7 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
test "default sendmail settings" do
settings = {
location: '/usr/sbin/sendmail',
- arguments: '-i -t'
+ arguments: '-i'
}
assert_equal settings, ActionMailer::Base.sendmail_settings
end
diff --git a/actionmailer/test/fixtures/caching_mailer/_partial.html.erb b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb
new file mode 100644
index 0000000000..8e965f52b4
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/_partial.html.erb
@@ -0,0 +1,3 @@
+<% cache :caching do %>
+ Old fragment caching in a partial
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb
new file mode 100644
index 0000000000..90189627da
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache.html.erb
@@ -0,0 +1,3 @@
+<% cache :caching do %>
+"Welcome"
+<% end %>
diff --git a/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb
new file mode 100644
index 0000000000..2957d083e8
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/fragment_cache_in_partials.html.erb
@@ -0,0 +1 @@
+<%= render "partial" %>
diff --git a/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb
new file mode 100644
index 0000000000..0d52429a81
--- /dev/null
+++ b/actionmailer/test/fixtures/caching_mailer/skip_fragment_cache_digesting.html.erb
@@ -0,0 +1,3 @@
+<%= cache :no_digest, skip_digest: true do %>
+ No Digest
+<% end %>
diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index 6124ffeb52..50c4b74eb8 100644
--- a/actionmailer/test/i18n_with_controller_test.rb
+++ b/actionmailer/test/i18n_with_controller_test.rb
@@ -25,7 +25,9 @@ end
class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
class RoutedRackApp
diff --git a/actionmailer/test/mailers/caching_mailer.rb b/actionmailer/test/mailers/caching_mailer.rb
new file mode 100644
index 0000000000..345d267a36
--- /dev/null
+++ b/actionmailer/test/mailers/caching_mailer.rb
@@ -0,0 +1,15 @@
+class CachingMailer < ActionMailer::Base
+ self.mailer_name = "caching_mailer"
+
+ def fragment_cache
+ mail(subject: "welcome", template_name: "fragment_cache")
+ end
+
+ def fragment_cache_in_partials
+ mail(subject: "welcome", template_name: "fragment_cache_in_partials")
+ end
+
+ def skip_fragment_cache_digesting
+ mail(subject: "welcome", template_name: "skip_fragment_cache_digesting")
+ end
+end
diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb
index b834cdd08c..f06d69369f 100644
--- a/actionmailer/test/message_delivery_test.rb
+++ b/actionmailer/test/message_delivery_test.rb
@@ -93,4 +93,12 @@ class MessageDeliveryTest < ActiveSupport::TestCase
@mail.deliver_later(queue: :another_queue)
end
end
+
+ test 'deliver_later after accessing the message is disallowed' do
+ @mail.message # Load the message, which calls the mailer method.
+
+ assert_raise RuntimeError do
+ @mail.deliver_later
+ end
+ end
end
diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb
index 7928fe9542..70bd05055f 100644
--- a/actionmailer/test/url_test.rb
+++ b/actionmailer/test/url_test.rb
@@ -79,9 +79,11 @@ class ActionMailerUrlTest < ActionMailer::TestCase
UrlTestMailer.delivery_method = :test
AppRoutes.draw do
- get ':controller(/:action(/:id))'
- get '/welcome' => 'foo#bar', as: 'welcome'
- get '/dummy_model' => 'foo#baz', as: 'dummy_model'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ get '/welcome' => 'foo#bar', as: 'welcome'
+ get '/dummy_model' => 'foo#baz', as: 'dummy_model'
+ end
end
# string
@@ -108,8 +110,10 @@ class ActionMailerUrlTest < ActionMailer::TestCase
UrlTestMailer.delivery_method = :test
AppRoutes.draw do
- get ':controller(/:action(/:id))'
- get '/welcome' => "foo#bar", as: "welcome"
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ get '/welcome' => "foo#bar", as: "welcome"
+ end
end
expected = new_mail
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 921aff300f..370e3a1958 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,3 +1,100 @@
+* Add extension synonyms `yml` and `yaml` for MIME type `application/x-yaml`.
+
+ *bogdanvlviv*
+
+* Adds support for including ActionController::Cookies in API controllers.
+ Previously, including the module would raise when trying to define
+ a `cookies` helper method. Skip calling #helper_method if it is not
+ defined -- if we don't have helpers, we needn't define one.
+
+ Fixes #24304
+
+ *Ryan T. Hosford*
+
+* ETags: Introduce `Response#strong_etag=` and `#weak_etag=` and analogous
+ options for `fresh_when` and `stale?`. `Response#etag=` sets a weak ETag.
+
+ Strong ETags are desirable when you're serving byte-for-byte identical
+ responses that support Range requests, like PDFs or videos (typically
+ done by reproxying the response from a backend storage service).
+ Also desirable when fronted by some CDNs that support strong ETags
+ only, like Akamai.
+
+ *Jeremy Daer*
+
+* ETags: No longer strips quotes (") from ETag values before comparing them.
+ Quotes are significant, part of the ETag. A quoted ETag and an unquoted
+ one are not the same entity.
+
+ *Jeremy Daer*
+
+* ETags: Support `If-None-Match: *`. Rarely useful for GET requests; meant
+ to provide some optimistic concurrency control for PUT requests.
+
+ *Jeremy Daer*
+
+* `ActionDispatch::ParamsParser` is deprecated and was removed from the middleware
+ stack. To configure the parameter parsers use `ActionDispatch::Request.parameter_parsers=`.
+
+ *tenderlove*
+
+* When a `respond_to` collector with a block doesn't have a response, then
+ a `:no_content` response should be rendered. This brings the default
+ rendering behavior introduced by https://github.com/rails/rails/issues/19036
+ to controller methods employing `respond_to`.
+
+ *Justin Coyne*
+
+* Add `ActionController::Parameters#dig` on Ruby 2.3 and greater, which
+ behaves the same as `Hash#dig`.
+
+ *Sean Griffin*
+
+* Add request headers in the payload of the `start_processing.action_controller`
+ and `process_action.action_controller` notifications.
+
+ *Gareth du Plooy*
+
+* Add `action_dispatch_integration_test` load hook. The hook can be used to
+ extend `ActionDispatch::IntegrationTest` once it has been loaded.
+
+ *Yuichiro Kaneko*
+
+* Update default rendering policies when the controller action did
+ not explicitly indicate a response.
+
+ For API controllers, the implicit render always renders "204 No Content"
+ and does not account for any templates.
+
+ For other controllers, the following conditions are checked:
+
+ First, if a template exists for the controller action, it is rendered.
+ This template lookup takes into account the action name, locales, format,
+ variant, template handlers, etc. (see `render` for details).
+
+ Second, if other templates exist for the controller action but is not in
+ the right format (or variant, etc.), an `ActionController::UnknownFormat`
+ is raised. The list of available templates is assumed to be a complete
+ enumeration of all the possible formats (or variants, etc.); that is,
+ having only HTML and JSON templates indicate that the controller action is
+ not meant to handle XML requests.
+
+ Third, if the current request is an "interactive" browser request (the user
+ navigated here by entering the URL in the address bar, submitting a form,
+ clicking on a link, etc. as opposed to an XHR or non-browser API request),
+ `ActionView::UnknownFormat` is raised to display a helpful error
+ message.
+
+ Finally, it falls back to the same "204 No Content" behavior as API controllers.
+
+ *Godfrey Chan*, *Jon Moss*, *Kasper Timm Hansen*, *Mike Clark*, *Matthew Draper*
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* Add "application/gzip" as a default mime type.
+
+ *Mehmet Emin İNAÇ*
+
* Add request encoding and response parsing to integration tests.
What previously was:
@@ -13,7 +110,7 @@
headers: { 'Content-Type' => 'application/json' }
end
- assert_equal({ id: Article.last.id, title: 'Ahoy!' }, JSON.parse(response.body))
+ assert_equal({ 'id' => Article.last.id, 'title' => 'Ahoy!' }, JSON.parse(response.body))
end
end
```
@@ -26,30 +123,39 @@
class ApiTest < ActionDispatch::IntegrationTest
test 'creates articles' do
assert_difference -> { Article.count } do
- post articles_path, { article: { title: 'Ahoy!' } }, as: :json
+ post articles_path, params: { article: { title: 'Ahoy!' } }, as: :json
end
- assert_equal({ id: Article.last.id, title: 'Ahoy!' }, response.parsed_body)
+ assert_equal({ 'id' => Article.last.id, 'title' => 'Ahoy!' }, response.parsed_body)
end
end
```
+ Passing `as: :json` to integration test request helpers will set the format,
+ content type and encode the parameters as JSON.
+
+ Then on the response side, `parsed_body` will parse the body according to the
+ content type the response has.
+
+ Currently JSON is the only supported MIME type. Add your own with
+ `ActionDispatch::IntegrationTest.register_encoder`.
+
*Kasper Timm Hansen*
-* Add image/svg+xml as a default mime type.
+* Add "image/svg+xml" as a default mime type.
*DHH*
## Rails 5.0.0.beta2 (February 01, 2016) ##
-* Add `-g` and `-c` options to `bin/rake routes`. These options return the url `name`, `verb` and
+* Add `-g` and `-c` options to `bin/rails routes`. These options return the url `name`, `verb` and
`path` field that match the pattern or match a specific controller.
- Deprecate `CONTROLLER` env variable in `bin/rake routes`.
+ Deprecate `CONTROLLER` env variable in `bin/rails routes`.
See #18902.
- *Anton Davydov* & *Vipul A M*
+ *Anton Davydov*, *Vipul A M*
* Response etags to always be weak: Prefixes 'W/' to value returned by
`ActionDispatch::Http::Cache::Response#etag=`, such that etags set in
@@ -81,11 +187,7 @@
* Add option for per-form CSRF tokens.
- *Greg Ose & Ben Toews*
-
-* Add tests and documentation for `ActionController::Renderers::use_renderers`.
-
- *Benjamin Fleischer*
+ *Greg Ose*, *Ben Toews*
* Fix `ActionController::Parameters#convert_parameters_to_hashes` to return filtered
or unfiltered values based on from where it is called, `to_h` or `to_unsafe_h`
@@ -113,14 +215,14 @@
*Derek Prior*
-* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1
+* `ActionController::TestCase` will be moved to its own gem in Rails 5.1.
With the speed improvements made to `ActionDispatch::IntegrationTest` we no
longer need to keep two separate code bases for testing controllers. In
Rails 5.1 `ActionController::TestCase` will be deprecated and moved into a
gem outside of Rails source.
- This is a documentation deprecation so that going forward so new tests will use
+ This is a documentation deprecation so that going forward new tests will use
`ActionDispatch::IntegrationTest` instead of `ActionController::TestCase`.
*Eileen M. Uchitelle*
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index 28d8bc3091..66300754e3 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.homepage = 'http://rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
s.require_path = 'lib'
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index 56c4033387..1e57cbaac4 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -6,6 +6,7 @@ module AbstractController
extend ActiveSupport::Autoload
autoload :Base
+ autoload :Caching
autoload :Callbacks
autoload :Collector
autoload :DoubleRenderError, "abstract_controller/rendering"
@@ -15,4 +16,9 @@ module AbstractController
autoload :Translation
autoload :AssetPaths
autoload :UrlFor
+
+ def self.eager_load!
+ super
+ AbstractController::Caching.eager_load!
+ end
end
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index 8edea0f52b..16dec31938 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -1,13 +1,11 @@
require 'erubis'
+require 'abstract_controller/error'
require 'active_support/configurable'
require 'active_support/descendants_tracker'
require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/module/attr_internal'
module AbstractController
- class Error < StandardError #:nodoc:
- end
-
# Raised when a non-existing controller action is triggered.
class ActionNotFound < StandardError
end
diff --git a/actionpack/lib/abstract_controller/caching.rb b/actionpack/lib/abstract_controller/caching.rb
new file mode 100644
index 0000000000..0dea50889a
--- /dev/null
+++ b/actionpack/lib/abstract_controller/caching.rb
@@ -0,0 +1,62 @@
+module AbstractController
+ module Caching
+ extend ActiveSupport::Concern
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Fragments
+ end
+
+ module ConfigMethods
+ def cache_store
+ config.cache_store
+ end
+
+ def cache_store=(store)
+ config.cache_store = ActiveSupport::Cache.lookup_store(store)
+ end
+
+ private
+ def cache_configured?
+ perform_caching && cache_store
+ end
+ end
+
+ include ConfigMethods
+ include AbstractController::Caching::Fragments
+
+ included do
+ extend ConfigMethods
+
+ config_accessor :default_static_extension
+ self.default_static_extension ||= '.html'
+
+ config_accessor :perform_caching
+ self.perform_caching = true if perform_caching.nil?
+
+ class_attribute :_view_cache_dependencies
+ self._view_cache_dependencies = []
+ helper_method :view_cache_dependencies if respond_to?(:helper_method)
+ end
+
+ module ClassMethods
+ def view_cache_dependency(&dependency)
+ self._view_cache_dependencies += [dependency]
+ end
+ end
+
+ def view_cache_dependencies
+ self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
+ end
+
+ protected
+ # Convenience accessor.
+ def cache(key, options = {}, &block)
+ if cache_configured?
+ cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
+ else
+ yield
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/caching/fragments.rb b/actionpack/lib/abstract_controller/caching/fragments.rb
index b9ad51a9cf..3257a731ed 100644
--- a/actionpack/lib/action_controller/caching/fragments.rb
+++ b/actionpack/lib/abstract_controller/caching/fragments.rb
@@ -1,4 +1,4 @@
-module ActionController
+module AbstractController
module Caching
# Fragment caching is used for caching various blocks within
# views without caching the entire action as a whole. This is
@@ -135,13 +135,8 @@ module ActionController
end
def instrument_fragment_cache(name, key) # :nodoc:
- payload = {
- controller: controller_name,
- action: action_name,
- key: key
- }
-
- ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield }
+ payload = instrument_payload(key)
+ ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", payload) { yield }
end
end
end
diff --git a/actionpack/lib/abstract_controller/error.rb b/actionpack/lib/abstract_controller/error.rb
new file mode 100644
index 0000000000..7fafce4dd4
--- /dev/null
+++ b/actionpack/lib/abstract_controller/error.rb
@@ -0,0 +1,4 @@
+module AbstractController
+ class Error < StandardError #:nodoc:
+ end
+end
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index d84c238a62..ab4355296b 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -38,7 +38,8 @@ module AbstractController
end
# Declare a controller method as a helper. For example, the following
- # makes the +current_user+ controller method available to the view:
+ # makes the +current_user+ and +logged_in?+ controller methods available
+ # to the view:
# class ApplicationController < ActionController::Base
# helper_method :current_user, :logged_in?
#
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index e765d73ce4..9f192c54f7 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -1,3 +1,4 @@
+require 'abstract_controller/error'
require 'active_support/concern'
require 'active_support/core_ext/class/attribute'
require 'action_view'
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 40f33a9de0..62f5905205 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -9,12 +9,15 @@ module ActionController
autoload :API
autoload :Base
- autoload :Caching
autoload :Metal
autoload :Middleware
autoload :Renderer
autoload :FormBuilder
+ eager_autoload do
+ autoload :Caching
+ end
+
autoload_under "metal" do
autoload :ConditionalGet
autoload :Cookies
@@ -47,11 +50,6 @@ module ActionController
autoload :TestCase, 'action_controller/test_case'
autoload :TemplateAssertions, 'action_controller/test_case'
-
- def self.eager_load!
- super
- ActionController::Caching.eager_load!
- end
end
# Common Active Support usage in Action Controller
diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb
index ff12705abe..6bbebb7b4c 100644
--- a/actionpack/lib/action_controller/api.rb
+++ b/actionpack/lib/action_controller/api.rb
@@ -14,22 +14,22 @@ module ActionController
# flash, assets, and so on. This makes the entire controller stack thinner,
# suitable for API applications. It doesn't mean you won't have such
# features if you need them: they're all available for you to include in
- # your application, they're just not part of the default API Controller stack.
+ # your application, they're just not part of the default API controller stack.
#
- # By default, only the ApplicationController in a \Rails application inherits
- # from <tt>ActionController::API</tt>. All other controllers in turn inherit
- # from ApplicationController.
+ # Normally, +ApplicationController+ is the only controller that inherits from
+ # <tt>ActionController::API</tt>. All other controllers in turn inherit from
+ # +ApplicationController+.
#
# A sample controller could look like this:
#
# class PostsController < ApplicationController
# def index
- # @posts = Post.all
- # render json: @posts
+ # posts = Post.all
+ # render json: posts
# end
# end
#
- # Request, response and parameters objects all work the exact same way as
+ # Request, response, and parameters objects all work the exact same way as
# <tt>ActionController::Base</tt>.
#
# == Renders
@@ -37,18 +37,18 @@ module ActionController
# The default API Controller stack includes all renderers, which means you
# can use <tt>render :json</tt> and brothers freely in your controllers. Keep
# in mind that templates are not going to be rendered, so you need to ensure
- # your controller is calling either <tt>render</tt> or <tt>redirect</tt> in
- # all actions, otherwise it will return 204 No Content response.
+ # your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
+ # all actions, otherwise it will return 204 No Content.
#
# def show
- # @post = Post.find(params[:id])
- # render json: @post
+ # post = Post.find(params[:id])
+ # render json: post
# end
#
# == Redirects
#
# Redirects are used to move from one action to another. You can use the
- # <tt>redirect</tt> method in your controllers in the same way as
+ # <tt>redirect_to</tt> method in your controllers in the same way as in
# <tt>ActionController::Base</tt>. For example:
#
# def create
@@ -56,7 +56,7 @@ module ActionController
# # do stuff here
# end
#
- # == Adding new behavior
+ # == Adding New Behavior
#
# In some scenarios you may want to add back some functionality provided by
# <tt>ActionController::Base</tt> that is not present by default in
@@ -72,18 +72,19 @@ module ActionController
#
# class PostsController < ApplicationController
# def index
- # @posts = Post.all
+ # posts = Post.all
#
# respond_to do |format|
- # format.json { render json: @posts }
- # format.xml { render xml: @posts }
+ # format.json { render json: posts }
+ # format.xml { render xml: posts }
# end
# end
# end
#
- # Quite straightforward. Make sure to check <tt>ActionController::Base</tt>
- # available modules if you want to include any other functionality that is
- # not provided by <tt>ActionController::API</tt> out of the box.
+ # Quite straightforward. Make sure to check the modules included in
+ # <tt>ActionController::Base</tt> if you want to use any other
+ # functionality that is not provided by <tt>ActionController::API</tt>
+ # out of the box.
class API < Metal
abstract!
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index 0b8fa2ea09..a9a8508abc 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -1,6 +1,3 @@
-require 'fileutils'
-require 'uri'
-
module ActionController
# \Caching is a cheap way of speeding up slow applications by keeping the result of
# calculations, renderings, and database calls around for subsequent requests.
@@ -23,65 +20,25 @@ module ActionController
# config.action_controller.cache_store = :mem_cache_store, Memcached::Rails.new('localhost:11211')
# config.action_controller.cache_store = MyOwnStore.new('parameter')
module Caching
- extend ActiveSupport::Concern
extend ActiveSupport::Autoload
-
- eager_autoload do
- autoload :Fragments
- end
-
- module ConfigMethods
- def cache_store
- config.cache_store
- end
-
- def cache_store=(store)
- config.cache_store = ActiveSupport::Cache.lookup_store(store)
- end
-
- private
- def cache_configured?
- perform_caching && cache_store
- end
- end
-
- include AbstractController::Callbacks
-
- include ConfigMethods
- include Fragments
+ extend ActiveSupport::Concern
included do
- extend ConfigMethods
-
- config_accessor :default_static_extension
- self.default_static_extension ||= '.html'
-
- config_accessor :perform_caching
- self.perform_caching = true if perform_caching.nil?
-
- class_attribute :_view_cache_dependencies
- self._view_cache_dependencies = []
- helper_method :view_cache_dependencies if respond_to?(:helper_method)
+ include AbstractController::Caching
end
- module ClassMethods
- def view_cache_dependency(&dependency)
- self._view_cache_dependencies += [dependency]
- end
- end
+ private
- def view_cache_dependencies
- self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
- end
+ def instrument_payload(key)
+ {
+ controller: controller_name,
+ action: action_name,
+ key: key
+ }
+ end
- protected
- # Convenience accessor.
- def cache(key, options = {}, &block)
- if cache_configured?
- cache_store.fetch(ActiveSupport::Cache.expand_cache_key(key, :controller), options, &block)
- else
- yield
- end
+ def instrument_name
+ "action_controller"
end
end
end
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index 1641d01c30..f6e67b02d7 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -175,10 +175,7 @@ module ActionController
body = [body] unless body.nil? || body.respond_to?(:each)
response.reset_body!
return unless body
- body.each { |part|
- next if part.empty?
- response.write part
- }
+ response.body = body
super
end
diff --git a/actionpack/lib/action_controller/metal/basic_implicit_render.rb b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
index 6c6f8381ff..cef65a362c 100644
--- a/actionpack/lib/action_controller/metal/basic_implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/basic_implicit_render.rb
@@ -1,5 +1,5 @@
module ActionController
- module BasicImplicitRender
+ module BasicImplicitRender # :nodoc:
def send_action(method, *args)
super.tap { default_render unless performed? }
end
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index f8e0d9cf6c..480e265e44 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -36,8 +36,23 @@ module ActionController
#
# === Parameters:
#
- # * <tt>:etag</tt>.
- # * <tt>:last_modified</tt>.
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
+ # +:weak_etag+ option.
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
+ # equivalence, not byte-for-byte equality, so they're good for caching
+ # HTML pages in browser caches. They can't be used for responses that
+ # must be byte-identical, like serving Range requests within a PDF file.
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A strong ETag implies exact
+ # equality: the response must match byte for byte. This is necessary for
+ # doing Range requests within a large video or PDF file, for example, or
+ # for compatibility with some CDNs that don't support weak ETags.
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
+ # response. Subsequent requests that set If-Modified-Since may return a
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cacheable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
@@ -86,12 +101,16 @@ module ActionController
#
# before_action { fresh_when @article, template: 'widgets/show' }
#
- def fresh_when(object = nil, etag: object, last_modified: nil, public: false, template: nil)
+ def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, template: nil)
+ weak_etag ||= etag || object unless strong_etag
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
- if etag || template
- response.etag = combine_etags(etag: etag, last_modified: last_modified,
- public: public, template: template)
+ if strong_etag
+ response.strong_etag = combine_etags strong_etag,
+ last_modified: last_modified, public: public, template: template
+ elsif weak_etag || template
+ response.weak_etag = combine_etags weak_etag,
+ last_modified: last_modified, public: public, template: template
end
response.last_modified = last_modified if last_modified
@@ -107,8 +126,23 @@ module ActionController
#
# === Parameters:
#
- # * <tt>:etag</tt>.
- # * <tt>:last_modified</tt>.
+ # * <tt>:etag</tt> Sets a "weak" ETag validator on the response. See the
+ # +:weak_etag+ option.
+ # * <tt>:weak_etag</tt> Sets a "weak" ETag validator on the response.
+ # requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A weak ETag indicates semantic
+ # equivalence, not byte-for-byte equality, so they're good for caching
+ # HTML pages in browser caches. They can't be used for responses that
+ # must be byte-identical, like serving Range requests within a PDF file.
+ # * <tt>:strong_etag</tt> Sets a "strong" ETag validator on the response.
+ # Requests that set If-None-Match header may return a 304 Not Modified
+ # response if it matches the ETag exactly. A strong ETag implies exact
+ # equality: the response must match byte for byte. This is necessary for
+ # doing Range requests within a large video or PDF file, for example, or
+ # for compatibility with some CDNs that don't support weak ETags.
+ # * <tt>:last_modified</tt> Sets a "weak" last-update validator on the
+ # response. Subsequent requests that set If-Modified-Since may return a
+ # 304 Not Modified response if last_modified <= If-Modified-Since.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cacheable by other devices (proxy caches).
# * <tt>:template</tt> By default, the template digest for the current
@@ -180,12 +214,12 @@ module ActionController
# super if stale? @article, template: 'widgets/show'
# end
#
- def stale?(object = nil, etag: object, last_modified: nil, public: nil, template: nil)
- fresh_when(object, etag: etag, last_modified: last_modified, public: public, template: template)
+ def stale?(object = nil, **freshness_kwargs)
+ fresh_when(object, **freshness_kwargs)
!request.fresh?(response)
end
- # Sets a HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
+ # Sets an HTTP 1.1 Cache-Control header. Defaults to issuing a +private+
# instruction, so that intermediate caches must not cache the response.
#
# expires_in 20.minutes
@@ -195,7 +229,7 @@ module ActionController
# This method will overwrite an existing Cache-Control header.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html for more possibilities.
#
- # The method will also ensure a HTTP Date header for client compatibility.
+ # The method will also ensure an HTTP Date header for client compatibility.
def expires_in(seconds, options = {})
response.cache_control.merge!(
:max_age => seconds,
@@ -208,7 +242,7 @@ module ActionController
response.date = Time.now unless response.date?
end
- # Sets a HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
+ # Sets an HTTP 1.1 Cache-Control header of <tt>no-cache</tt> so no caching should
# occur by the browser or intermediate caches (like caching proxy servers).
def expires_now
response.cache_control.replace(:no_cache => true)
@@ -216,26 +250,23 @@ module ActionController
# Cache or yield the block. The cache is supposed to never expire.
#
- # You can use this method when you have a HTTP response that never changes,
+ # You can use this method when you have an HTTP response that never changes,
# and the browser and proxies should cache it indefinitely.
#
# * +public+: By default, HTTP responses are private, cached only on the
# user's web browser. To allow proxies to cache the response, set +true+ to
# indicate that they can serve the cached response to all users.
- #
- # * +version+: the version passed as a key for the cache.
- def http_cache_forever(public: false, version: 'v1')
+ def http_cache_forever(public: false)
expires_in 100.years, public: public
- yield if stale?(etag: "#{version}-#{request.fullpath}",
+ yield if stale?(etag: request.fullpath,
last_modified: Time.new(2011, 1, 1).utc,
public: public)
end
private
- def combine_etags(options)
- etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
- etags.unshift options[:etag]
+ def combine_etags(validator, options)
+ [validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
end
end
end
diff --git a/actionpack/lib/action_controller/metal/cookies.rb b/actionpack/lib/action_controller/metal/cookies.rb
index f8efb2b076..44925641a1 100644
--- a/actionpack/lib/action_controller/metal/cookies.rb
+++ b/actionpack/lib/action_controller/metal/cookies.rb
@@ -3,7 +3,7 @@ module ActionController #:nodoc:
extend ActiveSupport::Concern
included do
- helper_method :cookies
+ helper_method :cookies if defined?(helper_method)
end
private
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 35be6d9300..53527c08b6 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -347,7 +347,12 @@ module ActionController
# private
# def authenticate
# authenticate_or_request_with_http_token do |token, options|
- # token == TOKEN
+ # # Compare the tokens in a time-constant manner, to mitigate
+ # # timing attacks.
+ # ActiveSupport::SecurityUtils.secure_compare(
+ # ::Digest::SHA256.hexdigest(token),
+ # ::Digest::SHA256.hexdigest(TOKEN)
+ # )
# end
# end
# end
diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb
index 17fcc2fa02..6192fc0f9c 100644
--- a/actionpack/lib/action_controller/metal/implicit_render.rb
+++ b/actionpack/lib/action_controller/metal/implicit_render.rb
@@ -1,29 +1,62 @@
+require 'active_support/core_ext/string/strip'
+
module ActionController
+ # Handles implicit rendering for a controller action that does not
+ # explicitly respond with +render+, +respond_to+, +redirect+, or +head+.
+ #
+ # For API controllers, the implicit response is always 204 No Content.
+ #
+ # For all other controllers, we use these heuristics to decide whether to
+ # render a template, raise an error for a missing template, or respond with
+ # 204 No Content:
+ #
+ # First, if we DO find a template, it's rendered. Template lookup accounts
+ # for the action name, locales, format, variant, template handlers, and more
+ # (see +render+ for details).
+ #
+ # Second, if we DON'T find a template but the controller action does have
+ # templates for other formats, variants, etc., then we trust that you meant
+ # to provide a template for this response, too, and we raise
+ # <tt>ActionController::UnknownFormat</tt> with an explanation.
+ #
+ # Third, if we DON'T find a template AND the request is a page load in a web
+ # browser (technically, a non-XHR GET request for an HTML response) where
+ # you reasonably expect to have rendered a template, then we raise
+ # <tt>ActionView::UnknownFormat</tt> with an explanation.
+ #
+ # Finally, if we DON'T find a template AND the request isn't a browser page
+ # load, then we implicitly respond with 204 No Content.
module ImplicitRender
+ # :stopdoc:
include BasicImplicitRender
- # Renders the template corresponding to the controller action, if it exists.
- # The action name, format, and variant are all taken into account.
- # For example, the "new" action with an HTML format and variant "phone"
- # would try to render the <tt>new.html+phone.erb</tt> template.
- #
- # If no template is found <tt>ActionController::BasicImplicitRender</tt>'s implementation is called, unless
- # a block is passed. In that case, it will override the super implementation.
- #
- # default_render do
- # head 404 # No template was found
- # end
def default_render(*args)
if template_exists?(action_name.to_s, _prefixes, variants: request.variant)
render(*args)
+ elsif any_templates?(action_name.to_s, _prefixes)
+ message = "#{self.class.name}\##{action_name} is missing a template " \
+ "for this request format and variant.\n" \
+ "\nrequest.formats: #{request.formats.map(&:to_s).inspect}" \
+ "\nrequest.variant: #{request.variant.inspect}"
+
+ raise ActionController::UnknownFormat, message
+ elsif interactive_browser_request?
+ message = "#{self.class.name}\##{action_name} is missing a template " \
+ "for this request format and variant.\n\n" \
+ "request.formats: #{request.formats.map(&:to_s).inspect}\n" \
+ "request.variant: #{request.variant.inspect}\n\n" \
+ "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."
+
+ raise ActionController::UnknownFormat, message
else
- if block_given?
- yield(*args)
- else
- logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
- super
- end
+ logger.info "No template found for #{self.class.name}\##{action_name}, rendering head :no_content" if logger
+ super
end
end
@@ -32,5 +65,10 @@ module ActionController
"default_render"
end
end
+
+ private
+ def interactive_browser_request?
+ request.get? && request.format == Mime[:html] && !request.xhr?
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index bf74b39ac4..885ea3fefd 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -19,6 +19,7 @@ module ActionController
:controller => self.class.name,
:action => self.action_name,
:params => request.filtered_parameters,
+ :headers => request.headers,
:format => request.format.ref,
:method => request.request_method,
:path => request.fullpath
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 173a14a1d2..2e89af1a5e 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -198,7 +198,7 @@ module ActionController #:nodoc:
_process_format(format)
_set_rendered_content_type format
response = collector.response
- response ? response.call : render({})
+ response.call if response
else
raise ActionController::UnknownFormat
end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index b13ba06962..3c7cc15627 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -84,7 +84,7 @@ module ActionController
# redirect_back fallback_location: proc { edit_post_url(@post) }
#
# All options that can be passed to <tt>redirect_to</tt> are accepted as
- # options and the behavior is indetical.
+ # options and the behavior is identical.
def redirect_back(fallback_location:, **args)
if referer = request.headers["Referer"]
redirect_to referer, **args
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 6586985ff5..5793e28175 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -213,7 +213,7 @@ module ActionController #:nodoc:
if !verified_request?
if logger && log_warning_on_csrf_failure
- logger.warn "Can't verify CSRF token authenticity"
+ logger.warn "Can't verify CSRF token authenticity."
end
handle_unverified_request
end
@@ -379,7 +379,8 @@ module ActionController #:nodoc:
def xor_byte_strings(s1, s2)
s2_bytes = s2.bytes
- s1.bytes.map.with_index { |c1, i| c1 ^ s2_bytes[i] }.pack('c*')
+ s1.each_byte.with_index { |c1, i| s2_bytes[i] ^= c1 }
+ s2_bytes.pack('C*')
end
# The form's authenticity parameter. Override to provide your own.
diff --git a/actionpack/lib/action_controller/metal/rescue.rb b/actionpack/lib/action_controller/metal/rescue.rb
index 81b9a7b9ed..0621a7368c 100644
--- a/actionpack/lib/action_controller/metal/rescue.rb
+++ b/actionpack/lib/action_controller/metal/rescue.rb
@@ -7,8 +7,12 @@ module ActionController #:nodoc:
include ActiveSupport::Rescuable
def rescue_with_handler(exception)
- if exception.cause && handler_for_rescue(exception.cause)
- exception = exception.cause
+ if exception.cause
+ handler_index = index_of_handler_for_rescue(exception) || Float::INFINITY
+ cause_handler_index = index_of_handler_for_rescue(exception.cause)
+ if cause_handler_index && cause_handler_index <= handler_index
+ exception = exception.cause
+ end
end
super(exception)
end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index d3382ef296..f9b80dd805 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -109,7 +109,7 @@ module ActionController
cattr_accessor :permit_all_parameters, instance_accessor: false
cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
- delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, :inspect,
+ delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?,
:as_json, to: :@parameters
# By default, never raise an UnpermittedParameters exception if these
@@ -122,16 +122,6 @@ module ActionController
cattr_accessor :always_permitted_parameters
self.always_permitted_parameters = %w( controller action )
- def self.const_missing(const_name)
- return super unless const_name == :NEVER_UNPERMITTED_PARAMS
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated.
- Use `ActionController::Parameters.always_permitted_parameters` instead.
- MSG
-
- always_permitted_parameters
- end
-
# Returns a new instance of <tt>ActionController::Parameters</tt>.
# Also, sets the +permitted+ attribute to the default value of
# <tt>ActionController::Parameters.permit_all_parameters</tt>.
@@ -154,17 +144,21 @@ module ActionController
end
# Returns true if another +Parameters+ object contains the same content and
- # permitted flag, or other Hash-like object contains the same content. This
- # override is in place so you can perform a comparison with `Hash`.
- def ==(other_hash)
- if other_hash.respond_to?(:permitted?)
- super
+ # permitted flag.
+ def ==(other)
+ if other.respond_to?(:permitted?)
+ self.permitted? == other.permitted? && self.parameters == other.parameters
+ elsif other.is_a?(Hash)
+ ActiveSupport::Deprecation.warn <<-WARNING.squish
+ Comparing equality between `ActionController::Parameters` and a
+ `Hash` is deprecated and will be removed in Rails 5.1. Please only do
+ comparisons between instances of `ActionController::Parameters`. If
+ you need to compare to a hash, first convert it using
+ `ActionController::Parameters#new`.
+ WARNING
+ @parameters == other.with_indifferent_access
else
- if other_hash.is_a?(Hash)
- @parameters == other_hash.with_indifferent_access
- else
- @parameters == other_hash
- end
+ @parameters == other
end
end
@@ -190,6 +184,13 @@ module ActionController
# Returns an unsafe, unfiltered
# <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this
# parameter.
+ #
+ # params = ActionController::Parameters.new({
+ # name: 'Senjougahara Hitagi',
+ # oddity: 'Heavy stone crab'
+ # })
+ # params.to_unsafe_h
+ # # => {"name"=>"Senjougahara Hitagi", "oddity" => "Heavy stone crab"}
def to_unsafe_h
convert_parameters_to_hashes(@parameters, :to_unsafe_h)
end
@@ -436,6 +437,21 @@ module ActionController
)
end
+ if Hash.method_defined?(:dig)
+ # Extracts the nested parameter from the given +keys+ by calling +dig+
+ # at each step. Returns +nil+ if any intermediate step is +nil+.
+ #
+ # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } })
+ # params.dig(:foo, :bar, :baz) # => 1
+ # params.dig(:foo, :zot, :xyz) # => nil
+ #
+ # params2 = ActionController::Parameters.new(foo: [10, 11, 12])
+ # params2.dig(:foo, 1) # => 11
+ def dig(*keys)
+ convert_value_to_parameters(@parameters.dig(*keys))
+ end
+ end
+
# Returns a new <tt>ActionController::Parameters</tt> instance that
# includes only the given +keys+. If the given +keys+
# don't exist, returns an empty hash.
@@ -584,6 +600,10 @@ module ActionController
dup
end
+ def inspect
+ "<#{self.class} #{@parameters} permitted: #{@permitted}>"
+ end
+
def method_missing(method_sym, *args, &block)
if @parameters.respond_to?(method_sym)
message = <<-DEPRECATE.squish
@@ -603,12 +623,14 @@ module ActionController
end
protected
+ attr_reader :parameters
+
def permitted=(new_permitted)
@permitted = new_permitted
end
def fields_for_style?
- @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
+ @parameters.all? { |k, v| k =~ /\A-?\d+\z/ && (v.is_a?(Hash) || v.is_a?(Parameters)) }
end
private
@@ -734,6 +756,10 @@ module ActionController
end
end
+ def non_scalar?(value)
+ value.is_a?(Array) || value.is_a?(Parameters)
+ end
+
EMPTY_ARRAY = []
def hash_filter(params, filter)
filter = filter.with_indifferent_access
@@ -748,7 +774,7 @@ module ActionController
array_of_permitted_scalars?(self[key]) do |val|
params[key] = val
end
- else
+ elsif non_scalar?(value)
# Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
params[key] = each_element(value) do |element|
element.permit(*Array.wrap(filter[key]))
@@ -799,7 +825,8 @@ module ActionController
# end
#
# In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
- # will need to specify which nested attributes should be whitelisted.
+ # will need to specify which nested attributes should be whitelisted. You might want
+ # to allow +:id+ and +:_destroy+, see ActiveRecord::NestedAttributes for more information.
#
# class Person
# has_many :pets
@@ -819,7 +846,7 @@ module ActionController
# # It's mandatory to specify the nested attributes that should be whitelisted.
# # If you use `permit` with just the key that points to the nested attributes hash,
# # it will return an empty hash.
- # params.require(:person).permit(:name, :age, pets_attributes: [ :name, :category ])
+ # params.require(:person).permit(:name, :age, pets_attributes: [ :id, :name, :category ])
# end
# end
#
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index 0c4b661214..ecd21f29ce 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -52,7 +52,7 @@ module ActionController
self.session = session
self.session_options = TestSession::DEFAULT_OPTIONS
@custom_param_parsers = {
- Mime[:xml] => lambda { |raw_post| Hash.from_xml(raw_post)['hash'] }
+ xml: lambda { |raw_post| Hash.from_xml(raw_post)['hash'] }
}
end
@@ -105,7 +105,7 @@ module ActionController
when :url_encoded_form
data = non_path_parameters.to_query
else
- @custom_param_parsers[content_mime_type] = ->(_) { non_path_parameters }
+ @custom_param_parsers[content_mime_type.symbol] = ->(_) { non_path_parameters }
data = non_path_parameters.to_query
end
end
@@ -428,7 +428,7 @@ module ActionController
end
alias xhr :xml_http_request
- # Simulate a HTTP request to +action+ by specifying request method,
+ # Simulate an HTTP request to +action+ by specifying request method,
# parameters and set/volley the response.
#
# - +action+: The controller action to call.
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 1e4df07d6e..01d49475de 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -51,8 +51,8 @@ module ActionDispatch
autoload :Cookies
autoload :DebugExceptions
autoload :ExceptionWrapper
+ autoload :Executor
autoload :Flash
- autoload :LoadInterlock
autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 4bd727c14e..9fa2e38ae3 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -17,9 +17,7 @@ module ActionDispatch
end
def if_none_match_etags
- (if_none_match ? if_none_match.split(/\s*,\s*/) : []).collect do |etag|
- etag.gsub(/^\"|\"$/, "")
- end
+ if_none_match ? if_none_match.split(/\s*,\s*/) : []
end
def not_modified?(modified_at)
@@ -28,8 +26,8 @@ module ActionDispatch
def etag_matches?(etag)
if etag
- etag = etag.gsub(/^\"|\"$/, "")
- if_none_match_etags.include?(etag)
+ validators = if_none_match_etags
+ validators.include?(etag) || validators.include?('*')
end
end
@@ -80,27 +78,63 @@ module ActionDispatch
set_header DATE, utc_time.httpdate
end
- # This method allows you to set the ETag for cached content, which
- # will be returned to the end user.
+ # This method sets a weak ETag validator on the response so browsers
+ # and proxies may cache the response, keyed on the ETag. On subsequent
+ # requests, the If-None-Match header is set to the cached ETag. If it
+ # matches the current ETag, we can return a 304 Not Modified response
+ # with no body, letting the browser or proxy know that their cache is
+ # current. Big savings in request time and network bandwidth.
+ #
+ # Weak ETags are considered to be semantically equivalent but not
+ # byte-for-byte identical. This is perfect for browser caching of HTML
+ # pages where we don't care about exact equality, just what the user
+ # is viewing.
#
- # By default, Action Dispatch sets all ETags to be weak.
- # This ensures that if the content changes only semantically,
- # the whole page doesn't have to be regenerated from scratch
- # by the web server. With strong ETags, pages are compared
- # byte by byte, and are regenerated only if they are not exactly equal.
- def etag=(etag)
- key = ActiveSupport::Cache.expand_cache_key(etag)
- super %(W/"#{Digest::MD5.hexdigest(key)}")
+ # Strong ETags are considered byte-for-byte identical. They allow a
+ # browser or proxy cache to support Range requests, useful for paging
+ # through a PDF file or scrubbing through a video. Some CDNs only
+ # support strong ETags and will ignore weak ETags entirely.
+ #
+ # Weak ETags are what we almost always need, so they're the default.
+ # Check out `#strong_etag=` to provide a strong ETag validator.
+ def etag=(weak_validators)
+ self.weak_etag = weak_validators
+ end
+
+ def weak_etag=(weak_validators)
+ set_header 'ETag', generate_weak_etag(weak_validators)
+ end
+
+ def strong_etag=(strong_validators)
+ set_header 'ETag', generate_strong_etag(strong_validators)
end
def etag?; etag; end
+ # True if an ETag is set and it's a weak validator (preceded with W/)
+ def weak_etag?
+ etag? && etag.starts_with?('W/"')
+ end
+
+ # True if an ETag is set and it isn't a weak validator (not preceded with W/)
+ def strong_etag?
+ etag? && !weak_etag?
+ end
+
private
DATE = 'Date'.freeze
LAST_MODIFIED = "Last-Modified".freeze
SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public private must-revalidate])
+ def generate_weak_etag(validators)
+ "W/#{generate_strong_etag(validators)}"
+ end
+
+ def generate_strong_etag(validators)
+ %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}")
+ end
+
def cache_control_segments
if cache_control = _cache_control
cache_control.delete(' ').split(',')
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 9dcab79c3a..041eca48ca 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -4,9 +4,11 @@ module ActionDispatch
module Http
# Allows you to specify sensitive parameters which will be replaced from
# the request log by looking in the query string of the request and all
- # sub-hashes of the params hash to filter. If a block is given, each key and
- # value of the params hash and all sub-hashes is passed to it, the value
- # or key can be replaced using String#replace or similar method.
+ # sub-hashes of the params hash to filter. Filtering only certain sub-keys
+ # from a hash is possible by using the dot notation: 'credit_card.number'.
+ # If a block is given, each key and value of the params hash and all
+ # sub-hashes is passed to it, the value or key can be replaced using
+ # String#replace or similar method.
#
# env["action_dispatch.parameter_filter"] = [:password]
# => replaces the value to all keys matching /password/i with "[FILTERED]"
@@ -14,6 +16,10 @@ module ActionDispatch
# env["action_dispatch.parameter_filter"] = [:foo, "bar"]
# => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
#
+ # env["action_dispatch.parameter_filter"] = [ "credit_card.code" ]
+ # => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
+ # change { file: { code: "xxxx"} }
+ #
# env["action_dispatch.parameter_filter"] = -> (k, v) do
# v.reverse! if k =~ /secret/i
# end
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index 8e899174c6..69a934b7cd 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -5,7 +5,7 @@ module ActionDispatch
# env = { "CONTENT_TYPE" => "text/plain", "HTTP_USER_AGENT" => "curl/7.43.0" }
# headers = ActionDispatch::Http::Headers.new(env)
# headers["Content-Type"] # => "text/plain"
- # headers["User-Agent"] # => "curl/7/43/0"
+ # headers["User-Agent"] # => "curl/7.43.0"
#
# Also note that when headers are mapped to CGI-like variables by the Rack
# server, both dashes and underscores are converted to underscores. This
@@ -115,7 +115,7 @@ module ActionDispatch
private
- # Converts a HTTP header name to an environment variable name if it is
+ # Converts an HTTP header name to an environment variable name if it is
# not contained within the headers hash.
def env_name(key)
key = key.to_s
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index 8356d1a238..8b04174f1f 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -21,14 +21,15 @@ Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe)
Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml )
Mime::Type.register "application/rss+xml", :rss
Mime::Type.register "application/atom+xml", :atom
-Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml )
+Mime::Type.register "application/x-yaml", :yaml, %w( text/yaml ), %w(yml yaml)
Mime::Type.register "multipart/form-data", :multipart_form
Mime::Type.register "application/x-www-form-urlencoded", :url_encoded_form
# http://www.ietf.org/rfc/rfc4627.txt
# http://www.json.org/JSONRequest.html
-Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest application/vnd.api+json )
+Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
Mime::Type.register "application/zip", :zip, [], %w(zip)
+Mime::Type.register "application/gzip", :gzip, %w(application/x-gzip), %w(gz)
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index cca7376ffa..ff5031d7d5 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -1,22 +1,31 @@
module ActionDispatch
module Http
module Parameters
+ extend ActiveSupport::Concern
+
PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
DEFAULT_PARSERS = {
- Mime[:json] => lambda { |raw_post|
+ Mime[:json].symbol => -> (raw_post) {
data = ActiveSupport::JSON.decode(raw_post)
data.is_a?(Hash) ? data : {:_json => data}
}
}
- def self.included(klass)
- class << klass
- attr_accessor :parameter_parsers
+ included do
+ class << self
+ attr_reader :parameter_parsers
end
- klass.parameter_parsers = DEFAULT_PARSERS
+ self.parameter_parsers = DEFAULT_PARSERS
end
+
+ module ClassMethods
+ def parameter_parsers=(parsers) # :nodoc:
+ @parameter_parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
+ end
+ end
+
# Returns both GET and POST \parameters in a single hash.
def parameters
params = get_header("action_dispatch.request.parameters")
@@ -51,7 +60,7 @@ module ActionDispatch
def parse_formatted_parameters(parsers)
return yield if content_length.zero?
- strategy = parsers.fetch(content_mime_type) { return yield }
+ strategy = parsers.fetch(content_mime_type.symbol) { return yield }
begin
strategy.call(raw_post)
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index 5427425ef7..b0ed681623 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -337,7 +337,6 @@ module ActionDispatch
else
self.session = {}
end
- self.flash = nil
end
def session=(session) #:nodoc:
@@ -403,6 +402,10 @@ module ActionDispatch
def commit_flash
end
+ def ssl?
+ super || scheme == 'wss'.freeze
+ end
+
private
def check_method(name)
HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
diff --git a/actionpack/lib/action_dispatch/journey/backwards.rb b/actionpack/lib/action_dispatch/journey/backwards.rb
deleted file mode 100644
index 3bd20fdf81..0000000000
--- a/actionpack/lib/action_dispatch/journey/backwards.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Rack # :nodoc:
- Mount = ActionDispatch::Journey::Router
- Mount::RouteSet = ActionDispatch::Journey::Router
- Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern
-end
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
index 35c2b1b86e..cfd6681dd1 100644
--- a/actionpack/lib/action_dispatch/journey/route.rb
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -3,7 +3,7 @@ module ActionDispatch
class Route # :nodoc:
attr_reader :app, :path, :defaults, :name, :precedence
- attr_reader :constraints
+ attr_reader :constraints, :internal
alias :conditions :constraints
module VerbMatchers
@@ -55,7 +55,7 @@ module ActionDispatch
##
# +path+ is a path constraint.
# +constraints+ is a hash of constraints to be applied to this route.
- def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence)
+ def initialize(name, app, path, constraints, required_defaults, defaults, request_method_match, precedence, internal = false)
@name = name
@app = app
@path = path
@@ -70,6 +70,7 @@ module ActionDispatch
@decorated_ast = nil
@precedence = precedence
@path_formatter = @path.build_formatter
+ @internal = internal
end
def ast
@@ -81,7 +82,7 @@ module ActionDispatch
end
def requirements # :nodoc:
- # needed for rails `rake routes`
+ # needed for rails `rails routes`
@defaults.merge(path.requirements).delete_if { |_,v|
/.+?/ == v
}
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
index f649588520..06cdce1724 100644
--- a/actionpack/lib/action_dispatch/journey/router.rb
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -16,9 +16,6 @@ module ActionDispatch
class RoutingError < ::StandardError # :nodoc:
end
- # :nodoc:
- VERSION = '2.0.0'
-
attr_accessor :routes
def initialize(routes)
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index f80df78582..c782779b34 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -7,7 +7,16 @@ module ActionDispatch
define_callbacks :call
class << self
- delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
+ def to_prepare(*args, &block)
+ ActiveSupport::Reloader.to_prepare(*args, &block)
+ end
+
+ def to_cleanup(*args, &block)
+ ActiveSupport::Reloader.to_complete(*args, &block)
+ end
+
+ deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead',
+ to_cleanup: 'use ActiveSupport::Reloader.to_complete instead'
def before(*args, &block)
set_callback(:call, :before, *args, &block)
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index 3477aa8b29..f2f3150b56 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -2,6 +2,7 @@ require 'active_support/core_ext/hash/keys'
require 'active_support/key_generator'
require 'active_support/message_verifier'
require 'active_support/json'
+require 'rack/utils'
module ActionDispatch
class Request
@@ -337,7 +338,7 @@ module ActionDispatch
end
def to_header
- @cookies.map { |k,v| "#{k}=#{v}" }.join ';'
+ @cookies.map { |k,v| "#{escape(k)}=#{escape(v)}" }.join '; '
end
def handle_options(options) #:nodoc:
@@ -419,6 +420,10 @@ module ActionDispatch
private
+ def escape(string)
+ ::Rack::Utils.escape(string)
+ end
+
def make_set_cookie_header(header)
header = @set_cookies.inject(header) { |m, (k, v)|
if write_cookie?(v)
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index b55c937e0c..51a471fb23 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -156,15 +156,20 @@ module ActionDispatch
trace = wrapper.framework_trace if trace.empty?
ActiveSupport::Deprecation.silence do
- message = "\n#{exception.class} (#{exception.message}):\n"
- message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
- message << " " << trace.join("\n ")
- logger.fatal("#{message}\n\n")
+ logger.fatal " "
+ logger.fatal "#{exception.class} (#{exception.message}):"
+ log_array logger, exception.annoted_source_code if exception.respond_to?(:annoted_source_code)
+ logger.fatal " "
+ log_array logger, trace
end
end
+ def log_array(logger, array)
+ array.map { |line| logger.fatal line }
+ end
+
def logger(request)
- request.logger || stderr_logger
+ request.logger || ActionView::Base.logger || stderr_logger
end
def stderr_logger
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index 3b61824cc9..59edc66086 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,4 +1,3 @@
-require 'action_controller/metal/exceptions'
require 'active_support/core_ext/module/attribute_accessors'
require 'rack/utils'
diff --git a/actionpack/lib/action_dispatch/middleware/executor.rb b/actionpack/lib/action_dispatch/middleware/executor.rb
new file mode 100644
index 0000000000..06245b403b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/executor.rb
@@ -0,0 +1,19 @@
+require 'rack/body_proxy'
+
+module ActionDispatch
+ class Executor
+ def initialize(app, executor)
+ @app, @executor = app, executor
+ end
+
+ def call(env)
+ state = @executor.run!
+ begin
+ response = @app.call(env)
+ returned = response << ::Rack::BodyProxy.new(response.pop) { state.complete! }
+ ensure
+ state.complete! unless returned
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index c51dcd542a..06038af571 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -70,6 +70,11 @@ module ActionDispatch
session.delete('flash')
end
end
+
+ def reset_session # :nodoc
+ super
+ self.flash = nil
+ end
end
class FlashNow #:nodoc:
diff --git a/actionpack/lib/action_dispatch/middleware/load_interlock.rb b/actionpack/lib/action_dispatch/middleware/load_interlock.rb
deleted file mode 100644
index 07f498319c..0000000000
--- a/actionpack/lib/action_dispatch/middleware/load_interlock.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require 'active_support/dependencies'
-require 'rack/body_proxy'
-
-module ActionDispatch
- class LoadInterlock
- def initialize(app)
- @app = app
- end
-
- def call(env)
- interlock = ActiveSupport::Dependencies.interlock
- interlock.start_running
- response = @app.call(env)
- body = Rack::BodyProxy.new(response[2]) { interlock.done_running }
- response[2] = body
- response
- ensure
- interlock.done_running unless body
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
index c2a4f46e67..faf3262b8f 100644
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb
@@ -37,6 +37,8 @@ module ActionDispatch
# The +parsers+ argument can take Hash of parsers where key is identifying
# content mime type, and value is a lambda that is going to process data.
def self.new(app, parsers = {})
+ ActiveSupport::Deprecation.warn('ActionDispatch::ParamsParser is deprecated and will be removed in Rails 5.1. Configure the parameter parsing in ActionDispatch::Request.parameter_parsers.')
+ parsers = parsers.transform_keys { |key| key.respond_to?(:symbol) ? key.symbol : key }
ActionDispatch::Request.parameter_parsers = ActionDispatch::Request::DEFAULT_PARSERS.merge(parsers)
app
end
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index af9a29eb07..112bde6596 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -23,74 +23,32 @@ module ActionDispatch
# middleware stack, but are executed only when <tt>ActionDispatch::Reloader.prepare!</tt>
# or <tt>ActionDispatch::Reloader.cleanup!</tt> are called manually.
#
- class Reloader
- include ActiveSupport::Callbacks
- include ActiveSupport::Deprecation::Reporting
-
- define_callbacks :prepare
- define_callbacks :cleanup
-
- # Add a prepare callback. Prepare callbacks are run before each request, prior
- # to ActionDispatch::Callback's before callbacks.
+ class Reloader < Executor
def self.to_prepare(*args, &block)
- unless block_given?
- warn "to_prepare without a block is deprecated. Please use a block"
- end
- set_callback(:prepare, *args, &block)
+ ActiveSupport::Reloader.to_prepare(*args, &block)
end
- # Add a cleanup callback. Cleanup callbacks are run after each request is
- # complete (after #close is called on the response body).
def self.to_cleanup(*args, &block)
- unless block_given?
- warn "to_cleanup without a block is deprecated. Please use a block"
- end
- set_callback(:cleanup, *args, &block)
+ ActiveSupport::Reloader.to_complete(*args, &block)
end
- # Execute all prepare callbacks.
def self.prepare!
- new(nil).prepare!
+ default_reloader.prepare!
end
- # Execute all cleanup callbacks.
def self.cleanup!
- new(nil).cleanup!
- end
-
- def initialize(app, condition=nil)
- @app = app
- @condition = condition || lambda { true }
- @validated = true
+ default_reloader.reload!
end
- def call(env)
- @validated = @condition.call
- prepare!
-
- response = @app.call(env)
- response[2] = ::Rack::BodyProxy.new(response[2]) { cleanup! }
+ class << self
+ attr_accessor :default_reloader # :nodoc:
- response
- rescue Exception
- cleanup!
- raise
+ deprecate to_prepare: 'use ActiveSupport::Reloader.to_prepare instead',
+ to_cleanup: 'use ActiveSupport::Reloader.to_complete instead',
+ prepare!: 'use Rails.application.reloader.prepare! instead',
+ cleanup!: 'use Rails.application.reloader.reload! instead of cleanup + prepare'
end
- def prepare! #:nodoc:
- run_callbacks :prepare if validated?
- end
-
- def cleanup! #:nodoc:
- run_callbacks :cleanup if validated?
- ensure
- @validated = true
- end
-
- private
-
- def validated? #:nodoc:
- @validated
- end
+ self.default_reloader = ActiveSupport::Reloader
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index 429a98f236..dec9c60ef2 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -23,7 +23,7 @@ module ActionDispatch
# goes a step further than signed cookies in that encrypted cookies cannot
# be altered or read by users. This is the default starting in Rails 4.
#
- # If you have both secret_token and secret_key base set, your cookies will
+ # If you have both secret_token and secret_key_base set, your cookies will
# be encrypted, and signed cookies generated by Rails 3 will be
# transparently read and encrypted to provide a smooth upgrade path.
#
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 735b5939dd..ab3077b308 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -23,7 +23,7 @@ module ActionDispatch
# preload lists is `18.weeks`.
# * `subdomains`: Set to `true` to tell the browser to apply these settings
# to all subdomains. This protects your cookies from interception by a
- # vulnerable site on a subdomain. Defaults to `false`.
+ # vulnerable site on a subdomain. Defaults to `true`.
# * `preload`: Advertise that this site may be included in browsers'
# preloaded HSTS lists. HSTS protects your site on every visit *except the
# first visit* since it hasn't seen your HSTS header yet. To close this
@@ -34,6 +34,10 @@ module ActionDispatch
# original HSTS directive until it expires. Instead, use the header to tell browsers to
# expire HSTS immediately. Setting `hsts: false` is a shortcut for
# `hsts: { expires: 0 }`.
+ #
+ # Requests can opt-out of redirection with `exclude`:
+ #
+ # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }
class SSL
# Default to 180 days, the low end for https://www.ssllabs.com/ssltest/
# and greater than the 18-week requirement for browser preload lists.
@@ -49,14 +53,26 @@ module ActionDispatch
if options[:host] || options[:port]
ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
The `:host` and `:port` options are moving within `:redirect`:
- `config.ssl_options = { redirect: { host: …, port: … }}`.
+ `config.ssl_options = { redirect: { host: …, port: … } }`.
end_warning
@redirect = options.slice(:host, :port)
else
@redirect = redirect
end
+ @exclude = @redirect && @redirect[:exclude] || proc { !@redirect }
@secure_cookies = secure_cookies
+
+ if hsts != true && hsts != false && hsts[:subdomains].nil?
+ hsts[:subdomains] = false
+
+ ActiveSupport::Deprecation.warn <<-end_warning.strip_heredoc
+ In Rails 5.1, The `:subdomains` option of HSTS config will be treated as true if
+ unspecified. Set `config.ssl_options = { hsts: { subdomains: false } }` to opt out
+ of this behavior.
+ end_warning
+ end
+
@hsts_header = build_hsts_header(normalize_hsts_options(hsts))
end
@@ -69,7 +85,7 @@ module ActionDispatch
flag_cookies_as_secure! headers if @secure_cookies
end
else
- return redirect_to_https request if @redirect
+ return redirect_to_https request unless @exclude.call(request)
@app.call(env)
end
end
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index ddeea24bb3..e9e6a2e597 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -39,6 +39,8 @@ module ActionDispatch
config.action_dispatch.always_write_cookie = Rails.env.development? if config.action_dispatch.always_write_cookie.nil?
ActionDispatch::Cookies::CookieJar.always_write_cookie = config.action_dispatch.always_write_cookie
+ ActionDispatch::Reloader.default_reloader = app.reloader
+
ActionDispatch.test_app = app
end
end
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 79f9283f83..79d2f1f13c 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -73,14 +73,14 @@ module ActionDispatch
# get 'post/:id' => 'posts#show'
# post 'post/:id' => '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.
+ #
# 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]
#
- # 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.
- #
# == Named routes
#
# Routes can be named by passing an <tt>:as</tt> option,
@@ -159,7 +159,7 @@ module ActionDispatch
#
# controller 'geocode' do
# get 'geocode/:postalcode' => :show, constraints: {
- # postalcode: /# Postcode format
+ # postalcode: /# Postalcode format
# \d{5} #Prefix
# (-\d{4})? #Suffix
# /x
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index b806ee015b..5d30a545a2 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -41,7 +41,7 @@ module ActionDispatch
end
def internal?
- controller.to_s =~ %r{\Arails/(info|mailers|welcome)}
+ internal
end
def engine?
@@ -51,7 +51,7 @@ module ActionDispatch
##
# This class is just used for displaying route information when someone
- # executes `rake routes` or looks at the RoutingError page.
+ # executes `rails routes` or looks at the RoutingError page.
# People should not use this class.
class RoutesInspector # :nodoc:
def initialize(routes)
@@ -84,14 +84,15 @@ module ActionDispatch
if filter.is_a?(Hash) && filter[:controller]
{ controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ }
elsif filter
- { controller: /#{filter}/, action: /#{filter}/ }
+ { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ }
end
end
def filter_routes(filter)
if filter
@routes.select do |route|
- filter.any? { |default, value| route.defaults[default] =~ value }
+ route_wrapper = RouteWrapper.new(route)
+ filter.any? { |default, value| route_wrapper.send(default) =~ value }
end
else
@routes
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index afbaa45d20..5a747b5f17 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -107,6 +107,7 @@ module ActionDispatch
@ast = ast
@anchor = anchor
@via = via
+ @internal = options[:internal]
path_params = ast.find_all(&:symbol?).map(&:to_sym)
@@ -148,7 +149,8 @@ module ActionDispatch
required_defaults,
defaults,
request_method,
- precedence)
+ precedence,
+ @internal)
route
end
@@ -1596,7 +1598,7 @@ module ActionDispatch
route_options = options.dup
if _path && option_path
ActiveSupport::Deprecation.warn <<-eowarn
-Specifying strings for both :path and the route path is deprecated. Change things like this:
+Specifying strings for both :path and the route path is deprecated. Change things like this:
match #{_path.inspect}, :path => #{option_path.inspect}
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 846b5fa1fc..16237bd564 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -289,7 +289,7 @@ module ActionDispatch
if last.permitted?
args.pop.to_h
else
- raise ArgumentError, "Generating an URL from non sanitized request parameters is insecure!"
+ raise ArgumentError, "Generating a URL from non sanitized request parameters is insecure!"
end
end
helper.call self, args, options
@@ -513,6 +513,21 @@ module ActionDispatch
route = @set.add_route(name, mapping)
named_routes[name] = route if name
+
+ if route.segment_keys.include?(:controller)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using a dynamic :controller segment in a route is deprecated and
+ will be removed in Rails 5.1.
+ MSG
+ end
+
+ if route.segment_keys.include?(:action)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Using a dynamic :action segment in a route is deprecated and
+ will be removed in Rails 5.1.
+ MSG
+ end
+
route
end
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index f91679593e..28be189f93 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -173,7 +173,7 @@ module ActionDispatch
route_name)
when ActionController::Parameters
unless options.permitted?
- raise ArgumentError.new("Generating an URL from non sanitized request parameters is insecure!")
+ raise ArgumentError.new("Generating a URL from non sanitized request parameters is insecure!")
end
route_name = options.delete :use_route
_routes.url_for(options.to_h.symbolize_keys.
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index e7af27463c..44ad2c10d8 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -117,7 +117,7 @@ module ActionDispatch
# # Tests a route, providing a defaults hash
# assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
#
- # # Tests a route with a HTTP method
+ # # Tests a route with an HTTP method
# assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message)
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 8a8e22053a..69ae5a8468 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -95,7 +95,7 @@ module ActionDispatch
ActiveSupport::Deprecation.warn(<<-MSG.strip_heredoc)
xhr and xml_http_request methods are deprecated in favor of
- `get "/posts", xhr: true` and `post "/posts/1", xhr: true`
+ `get "/posts", xhr: true` and `post "/posts/1", xhr: true`.
MSG
process(request_method, path, params: params, headers: headers, xhr: true)
@@ -729,39 +729,55 @@ module ActionDispatch
# response_parser: -> body { body }
#
# Where `param_encoder` defines how the params should be encoded and
- # `response_parser` defines how the response body should be parsed.
+ # `response_parser` defines how the response body should be parsed through
+ # `parsed_body`.
#
# Consult the Rails Testing Guide for more.
class IntegrationTest < ActiveSupport::TestCase
- include Integration::Runner
- include ActionController::TemplateAssertions
- include ActionDispatch::Routing::UrlFor
+ module UrlOptions
+ extend ActiveSupport::Concern
+ def url_options
+ integration_session.url_options
+ end
+ end
- @@app = nil
+ module Behavior
+ extend ActiveSupport::Concern
- def self.app
- @@app || ActionDispatch.test_app
- end
+ include Integration::Runner
+ include ActionController::TemplateAssertions
- def self.app=(app)
- @@app = app
- end
+ included do
+ include ActionDispatch::Routing::UrlFor
+ include UrlOptions # don't let UrlFor override the url_options method
+ ActiveSupport.run_load_hooks(:action_dispatch_integration_test, self)
+ @@app = nil
+ end
- def app
- super || self.class.app
- end
+ module ClassMethods
+ def app
+ defined?(@@app) ? @@app : ActionDispatch.test_app
+ end
- def url_options
- integration_session.url_options
- end
+ def app=(app)
+ @@app = app
+ end
- def document_root_element
- html_document.root
- end
+ def register_encoder(*args)
+ Integration::Session::RequestEncoder.register_encoder(*args)
+ end
+ end
- def self.register_encoder(*args)
- Integration::Session::RequestEncoder.register_encoder(*args)
+ def app
+ super || self.class.app
+ end
+
+ def document_root_element
+ html_document.root
+ end
end
+
+ include Behavior
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_response.rb b/actionpack/lib/action_dispatch/testing/test_response.rb
index 4f289ad4b5..9d4b73a43d 100644
--- a/actionpack/lib/action_dispatch/testing/test_response.rb
+++ b/actionpack/lib/action_dispatch/testing/test_response.rb
@@ -22,7 +22,7 @@ module ActionDispatch
attr_writer :response_parser # :nodoc:
def parsed_body
- @response_parser.call(body)
+ @parsed_body ||= @response_parser.call(body)
end
end
end
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
index 778c5482d3..157f401f54 100644
--- a/actionpack/lib/action_pack/gem_version.rb
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -8,7 +8,7 @@ module ActionPack
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 1ef10ff956..fcbbfe8a18 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -1,5 +1,3 @@
-require File.expand_path('../../../load_paths', __FILE__)
-
$:.unshift(File.dirname(__FILE__) + '/lib')
$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
$:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers')
@@ -69,7 +67,9 @@ FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
SharedTestRoutes.draw do
- get ':controller(/:action)'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action)'
+ end
end
module ActionDispatch
@@ -118,7 +118,9 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
self.app = build_app
app.routes.draw do
- get ':controller(/:action)'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action)'
+ end
end
class DeadEndRoutes < ActionDispatch::Routing::RouteSet
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index 899d92f815..db71aa2160 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -177,7 +177,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
set.draw do
get 'route_one', :to => 'action_pack_assertions#nothing', :as => :route_one
get 'route_two', :to => 'action_pack_assertions#nothing', :id => 'two', :as => :route_two
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
process :redirect_to_named_route
assert_raise(ActiveSupport::TestCase::Assertion) do
@@ -201,7 +204,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
with_routing do |set|
set.draw do
get 'admin/inner_module', :to => 'admin/inner_module#index', :as => :admin_inner_module
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
process :redirect_to_index
# redirection is <{"action"=>"index", "controller"=>"admin/admin/inner_module"}>
@@ -215,7 +221,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
with_routing do |set|
set.draw do
get '/action_pack_assertions/:id', :to => 'action_pack_assertions#index', :as => :top_level
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
process :redirect_to_top_level_named_route
# assert_redirected_to "http://test.host/action_pack_assertions/foo" would pass because of exact match early return
@@ -231,7 +240,10 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
set.draw do
# this controller exists in the admin namespace as well which is the only difference from previous test
get '/user/:id', :to => 'user#index', :as => :top_level
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
process :redirect_to_top_level_named_route
# assert_redirected_to top_level_url('foo') would pass because of exact match early return
diff --git a/actionpack/test/controller/api/with_cookies_test.rb b/actionpack/test/controller/api/with_cookies_test.rb
new file mode 100644
index 0000000000..4491dc9002
--- /dev/null
+++ b/actionpack/test/controller/api/with_cookies_test.rb
@@ -0,0 +1,21 @@
+require 'abstract_unit'
+
+class WithCookiesController < ActionController::API
+ include ActionController::Cookies
+
+ def with_cookies
+ render plain: cookies[:foobar]
+ end
+end
+
+class WithCookiesTest < ActionController::TestCase
+ tests WithCookiesController
+
+ def test_with_cookies
+ request.cookies[:foobar] = 'bazbang'
+
+ get :with_cookies
+
+ assert_equal 'bazbang', response.body
+ end
+end
diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb
index e3f669dbb5..577a3d5800 100644
--- a/actionpack/test/controller/base_test.rb
+++ b/actionpack/test/controller/base_test.rb
@@ -175,7 +175,10 @@ class UrlOptionsTest < ActionController::TestCase
with_routing do |set|
set.draw do
get 'from_view', :to => 'url_options#from_view', :as => :from_view
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
get :from_view, params: { route: "from_view_url" }
@@ -209,7 +212,10 @@ class DefaultUrlOptionsTest < ActionController::TestCase
with_routing do |set|
set.draw do
get 'from_view', :to => 'default_url_options#from_view', :as => :from_view
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
get :from_view, params: { route: "from_view_url" }
@@ -226,7 +232,10 @@ class DefaultUrlOptionsTest < ActionController::TestCase
scope("/:locale") do
resources :descriptions
end
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
get :from_view, params: { route: "description_path(1)" }
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index 74c78dfa8e..7faf3cd8c6 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -219,12 +219,15 @@ CACHED
end
def test_fragment_caching_with_options
+ time = Time.now
get :fragment_cached_with_options
assert_response :success
expected_body = "<body>\n<p>ERB</p>\n</body>\n"
assert_equal expected_body, @response.body
- assert_equal "<p>ERB</p>", @store.read("views/with_options")
+ Time.stub(:now, time + 11) do
+ assert_nil @store.read("views/with_options")
+ end
end
def test_render_inline_before_fragment_caching
@@ -381,14 +384,14 @@ class CollectionCacheController < ActionController::Base
render 'index'
end
- def index_explicit_render
+ def index_explicit_render_in_controller
@customers = [Customer.new('david', 1)]
- render partial: 'customers/customer', collection: @customers
+ render partial: 'customers/customer', collection: @customers, cached: true
end
def index_with_comment
@customers = [Customer.new('david', 1)]
- render partial: 'customers/commented_customer', collection: @customers, as: :customer
+ render partial: 'customers/commented_customer', collection: @customers, as: :customer, cached: true
end
end
@@ -399,12 +402,13 @@ class AutomaticCollectionCacheTest < ActionController::TestCase
@controller.perform_caching = true
@controller.partial_rendered_times = 0
@controller.cache_store = ActiveSupport::Cache::MemoryStore.new
- ActionView::PartialRenderer.collection_cache = @controller.cache_store
+ ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new
end
def test_collection_fetches_cached_views
get :index
assert_equal 1, @controller.partial_rendered_times
+ assert_customer_cached 'david/1', 'david, 1'
get :index
assert_equal 1, @controller.partial_rendered_times
@@ -412,13 +416,16 @@ class AutomaticCollectionCacheTest < ActionController::TestCase
def test_preserves_order_when_reading_from_cache_plus_rendering
get :index, params: { id: 2 }
- get :index_ordered
+ assert_equal 1, @controller.partial_rendered_times
+ assert_select ':root', 'david, 2'
+ get :index_ordered
+ assert_equal 3, @controller.partial_rendered_times
assert_select ':root', "david, 1\n david, 2\n david, 3"
end
def test_explicit_render_call_with_options
- get :index_explicit_render
+ get :index_explicit_render_in_controller
assert_select ':root', "david, 1"
end
@@ -430,6 +437,12 @@ class AutomaticCollectionCacheTest < ActionController::TestCase
get :index_with_comment
assert_equal 1, @controller.partial_rendered_times
end
+
+ private
+ def assert_customer_cached(key, content)
+ assert_match content,
+ ActionView::PartialRenderer.collection_cache.read("views/#{key}/7c228ab609f0baf0b1f2367469210937")
+ end
end
class FragmentCacheKeyTestController < CachingController
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index b063d769a4..eef48e8480 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -323,7 +323,9 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
def with_test_route_set
with_routing do |set|
set.draw do
- get ':action', :to => FlashIntegrationTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ':action', :to => FlashIntegrationTest::TestController
+ end
end
@app = self.class.build_app(set) do |middleware|
diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb
index 22f1cc7c22..03a9c9ae78 100644
--- a/actionpack/test/controller/force_ssl_test.rb
+++ b/actionpack/test/controller/force_ssl_test.rb
@@ -322,3 +322,12 @@ class RedirectToSSLTest < ActionController::TestCase
assert_equal 'ihaz', response.body
end
end
+
+class ForceSSLControllerLevelTest < ActionController::TestCase
+ def test_no_redirect_websocket_ssl_request
+ request.env['rack.url_scheme'] = 'wss'
+ request.env['Upgrade'] = 'websocket'
+ get :cheeseburger
+ assert_response 200
+ end
+end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index ea50f05f4d..ad7166bafa 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -390,7 +390,7 @@ class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest
reset!
%w( get post head patch put delete ).each do |verb|
- assert_nothing_raised("'#{verb}' should use integration test methods") { __send__(verb, '/') }
+ assert_nothing_raised { __send__(verb, '/') }
end
end
end
@@ -730,8 +730,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
set.draw do
get 'moved' => redirect('/method')
- match ':action', :to => controller, :via => [:get, :post], :as => :action
- get 'get/:action', :to => controller, :as => :get_action
+ ActiveSupport::Deprecation.silence do
+ match ':action', :to => controller, :via => [:get, :post], :as => :action
+ get 'get/:action', :to => controller, :as => :get_action
+ end
end
self.singleton_class.include(set.url_helpers)
@@ -1105,7 +1107,12 @@ class IntegrationRequestsWithoutSetup < ActionDispatch::IntegrationTest
def test_request
with_routing do |routes|
- routes.draw { get ':action' => FooController }
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ':action' => FooController
+ end
+ end
+
get '/ok'
assert_response 200
@@ -1173,7 +1180,11 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
def test_parsed_body_without_as_option
with_routing do |routes|
- routes.draw { get ':action' => FooController }
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ get ':action' => FooController
+ end
+ end
get '/foos_json.json', params: { foo: 'heyo' }
@@ -1184,7 +1195,11 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest
private
def post_to_foos(as:)
with_routing do |routes|
- routes.draw { post ':action' => FooController }
+ routes.draw do
+ ActiveSupport::Deprecation.silence do
+ post ':action' => FooController
+ end
+ end
post "/foos_#{as}", params: { foo: 'fighters' }, as: as
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index 0c3884cd38..a7759c080b 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -205,7 +205,7 @@ module ActionController
def overfill_buffer_and_die
logger = ActionController::Base.logger || Logger.new($stdout)
response.stream.on_error do
- logger.warn 'Error while streaming'
+ logger.warn 'Error while streaming.'
error_latch.count_down
end
diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb
index 6ae33be3c8..57cf2dafdf 100644
--- a/actionpack/test/controller/log_subscriber_test.rb
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -183,6 +183,12 @@ class ACLogSubscriberTest < ActionController::TestCase
assert_equal "test_value", @controller.last_payload[:test_key]
end
+ def test_process_action_headers
+ get :show
+ wait
+ assert_equal "Rails Testing", @controller.last_payload[:headers]['User-Agent']
+ end
+
def test_process_action_with_filter_parameters
@request.env["action_dispatch.parameter_filter"] = [:lifo, :amount]
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
index 76e2d3ff43..993f4001de 100644
--- a/actionpack/test/controller/mime/respond_to_test.rb
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -74,6 +74,14 @@ class RespondToController < ActionController::Base
end
end
+ def missing_templates
+ respond_to do |type|
+ # This test requires a block that is empty
+ type.json { }
+ type.xml
+ end
+ end
+
def using_defaults_with_type_list
respond_to(:html, :xml)
end
@@ -160,7 +168,14 @@ class RespondToController < ActionController::Base
end
end
- def variant_with_implicit_rendering
+ def variant_with_implicit_template_rendering
+ # This has exactly one variant template defined in the file system (+mobile.html.erb),
+ # which raises the regular MissingTemplate error for other variants.
+ end
+
+ def variant_without_implicit_template_rendering
+ # This differs from the above in that it does not have any templates defined in the file
+ # system, which triggers the ImplicitRender (204 No Content) behavior.
end
def variant_with_format_and_custom_render
@@ -272,6 +287,8 @@ class RespondToController < ActionController::Base
end
class RespondToControllerTest < ActionController::TestCase
+ NO_CONTENT_WARNING = "No template found for RespondToController#variant_without_implicit_template_rendering, rendering head :no_content"
+
def setup
super
@request.host = "www.example.com"
@@ -615,31 +632,77 @@ class RespondToControllerTest < ActionController::TestCase
end
end
+ def test_missing_templates
+ get :missing_templates, format: :json
+ assert_response :no_content
+ get :missing_templates, format: :xml
+ assert_response :no_content
+ end
+
def test_invalid_variant
+ assert_raises(ActionController::UnknownFormat) do
+ get :variant_with_implicit_template_rendering, params: { v: :invalid }
+ end
+ end
+
+ def test_variant_not_set_regular_unknown_format
+ assert_raises(ActionController::UnknownFormat) do
+ get :variant_with_implicit_template_rendering
+ end
+ end
+
+ def test_variant_with_implicit_template_rendering
+ get :variant_with_implicit_template_rendering, params: { v: :mobile }
+ assert_equal "text/html", @response.content_type
+ assert_equal "mobile", @response.body
+ end
+
+ def test_variant_without_implicit_rendering_from_browser
+ assert_raises(ActionController::UnknownFormat) do
+ get :variant_without_implicit_template_rendering, params: { v: :does_not_matter }
+ end
+ end
+
+ def test_variant_variant_not_set_and_without_implicit_rendering_from_browser
+ assert_raises(ActionController::UnknownFormat) do
+ get :variant_without_implicit_template_rendering
+ end
+ end
+
+ def test_variant_without_implicit_rendering_from_xhr
logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
- get :variant_with_implicit_rendering, params: { v: :invalid }
+ get :variant_without_implicit_template_rendering, xhr: true, params: { v: :does_not_matter }
assert_response :no_content
- assert_equal 1, logger.logged(:info).select{ |s| s =~ /No template found/ }.size, "Implicit head :no_content not logged"
+
+ assert_equal 1, logger.logged(:info).select{ |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
ensure
ActionController::Base.logger = old_logger
end
- def test_variant_not_set_regular_template_missing
- get :variant_with_implicit_rendering
+ def test_variant_without_implicit_rendering_from_api
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
+
+ get :variant_without_implicit_template_rendering, format: 'json', params: { v: :does_not_matter }
assert_response :no_content
+
+ assert_equal 1, logger.logged(:info).select{ |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
+ ensure
+ ActionController::Base.logger = old_logger
end
- def test_variant_with_implicit_rendering
- get :variant_with_implicit_rendering, params: { v: :implicit }
+ def test_variant_variant_not_set_and_without_implicit_rendering_from_xhr
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, logger
+
+ get :variant_without_implicit_template_rendering, xhr: true
assert_response :no_content
- end
- def test_variant_with_implicit_template_rendering
- get :variant_with_implicit_rendering, params: { v: :mobile }
- assert_equal "text/html", @response.content_type
- assert_equal "mobile", @response.body
+ assert_equal 1, logger.logged(:info).select { |s| s == NO_CONTENT_WARNING }.size, "Implicit head :no_content not logged"
+ ensure
+ ActionController::Base.logger = old_logger
end
def test_variant_with_format_and_custom_render
@@ -778,24 +841,3 @@ class RespondToControllerTest < ActionController::TestCase
assert_equal "phone", @response.body
end
end
-
-class RespondToWithBlockOnDefaultRenderController < ActionController::Base
- def show
- default_render do
- render body: 'default_render yielded'
- end
- end
-end
-
-class RespondToWithBlockOnDefaultRenderControllerTest < ActionController::TestCase
- def setup
- super
- @request.host = "www.example.com"
- end
-
- def test_default_render_uses_block_when_no_template_exists
- get :show
- assert_equal "default_render yielded", @response.body
- assert_equal "text/plain", @response.content_type
- end
-end
diff --git a/actionpack/test/controller/new_base/content_type_test.rb b/actionpack/test/controller/new_base/content_type_test.rb
index a9dcdde4b8..0b3a26807d 100644
--- a/actionpack/test/controller/new_base/content_type_test.rb
+++ b/actionpack/test/controller/new_base/content_type_test.rb
@@ -43,7 +43,9 @@ module ContentType
test "default response is text/plain and UTF8" do
with_routing do |set|
set.draw do
- get ':controller', :action => 'index'
+ ActiveSupport::Deprecation.silence do
+ get ':controller', :action => 'index'
+ end
end
get "/content_type/base"
diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb
index f4a3db8b41..c65c245773 100644
--- a/actionpack/test/controller/new_base/render_body_test.rb
+++ b/actionpack/test/controller/new_base/render_body_test.rb
@@ -85,7 +85,7 @@ module RenderBody
test "rendering body from an action with default options renders the body with the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
get "/render_body/simple"
assert_body "hello david"
@@ -95,7 +95,7 @@ module RenderBody
test "rendering body from an action with default options renders the body without the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
get "/render_body/with_layout"
diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb
index e9ea57e329..bfed136496 100644
--- a/actionpack/test/controller/new_base/render_html_test.rb
+++ b/actionpack/test/controller/new_base/render_html_test.rb
@@ -88,7 +88,7 @@ module RenderHtml
test "rendering text from an action with default options renders the text with the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
get "/render_html/simple"
assert_body "hello david"
@@ -98,7 +98,7 @@ module RenderHtml
test "rendering text from an action with default options renders the text without the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
get "/render_html/with_layout"
diff --git a/actionpack/test/controller/new_base/render_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb
index 0881442bd0..94afe7bcfe 100644
--- a/actionpack/test/controller/new_base/render_plain_test.rb
+++ b/actionpack/test/controller/new_base/render_plain_test.rb
@@ -80,7 +80,7 @@ module RenderPlain
test "rendering text from an action with default options renders the text with the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
get "/render_plain/simple"
assert_body "hello david"
@@ -90,7 +90,7 @@ module RenderPlain
test "rendering text from an action with default options renders the text without the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
get "/render_plain/with_layout"
diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb
index b06ce5db40..0d4c7cdb0a 100644
--- a/actionpack/test/controller/new_base/render_template_test.rb
+++ b/actionpack/test/controller/new_base/render_template_test.rb
@@ -177,7 +177,7 @@ module RenderTemplate
class TestWithLayout < Rack::TestCase
test "rendering with implicit layout" do
with_routing do |set|
- set.draw { get ':controller', :action => :index }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', :action => :index } }
get "/render_template/with_layout"
diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb
index 963f2c2f5c..1fb852a2c4 100644
--- a/actionpack/test/controller/new_base/render_test.rb
+++ b/actionpack/test/controller/new_base/render_test.rb
@@ -57,7 +57,9 @@ module Render
test "render with blank" do
with_routing do |set|
set.draw do
- get ":controller", :action => 'index'
+ ActiveSupport::Deprecation.silence do
+ get ":controller", :action => 'index'
+ end
end
get "/render/blank_render"
@@ -70,7 +72,9 @@ module Render
test "rendering more than once raises an exception" do
with_routing do |set|
set.draw do
- get ":controller", :action => 'index'
+ ActiveSupport::Deprecation.silence do
+ get ":controller", :action => 'index'
+ end
end
assert_raises(AbstractController::DoubleRenderError) do
diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb
index 048458178c..d4111d432c 100644
--- a/actionpack/test/controller/new_base/render_text_test.rb
+++ b/actionpack/test/controller/new_base/render_text_test.rb
@@ -83,7 +83,7 @@ module RenderText
test "rendering text from an action with default options renders the text with the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
ActiveSupport::Deprecation.silence do
get "/render_text/simple"
@@ -96,7 +96,7 @@ module RenderText
test "rendering text from an action with default options renders the text without the layout" do
with_routing do |set|
- set.draw { get ':controller', action: 'index' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller', action: 'index' } }
ActiveSupport::Deprecation.silence do
get "/render_text/with_layout"
diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
index bd43ff7697..17c62dc3fe 100644
--- a/actionpack/test/controller/parameters/accessors_test.rb
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -4,6 +4,8 @@ require 'active_support/core_ext/hash/transform_values'
class ParametersAccessorsTest < ActiveSupport::TestCase
setup do
+ ActionController::Parameters.permit_all_parameters = false
+
@params = ActionController::Parameters.new(
person: {
age: '32',
@@ -129,9 +131,87 @@ class ParametersAccessorsTest < ActiveSupport::TestCase
assert_not @params[:person].values_at(:name).first.permitted?
end
- test "equality with another hash works" do
+ test "equality with a hash is deprecated" do
hash1 = { foo: :bar }
params1 = ActionController::Parameters.new(hash1)
- assert(params1 == hash1)
+ assert_deprecated("will be removed in Rails 5.1") do
+ assert(params1 == hash1)
+ end
+ end
+
+ test "is equal to Parameters instance with same params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2)
+ params2 = ActionController::Parameters.new(a: 1, b: 2)
+ assert(params1 == params2)
+ end
+
+ test "is equal to Parameters instance with same permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ assert(params1 == params2)
+ end
+
+ test "is equal to Parameters instance with same different source params, but same permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1, c: 3).permit(:a)
+ assert(params1 == params2)
+ assert(params2 == params1)
+ end
+
+ test 'is not equal to an unpermitted Parameters instance with same params' do
+ params1 = ActionController::Parameters.new(a: 1).permit(:a)
+ params2 = ActionController::Parameters.new(a: 1)
+ assert(params1 != params2)
+ assert(params2 != params1)
+ end
+
+ test "is not equal to Parameters instance with different permitted params" do
+ params1 = ActionController::Parameters.new(a: 1, b: 2).permit(:a, :b)
+ params2 = ActionController::Parameters.new(a: 1, b: 2).permit(:a)
+ assert(params1 != params2)
+ assert(params2 != params1)
+ end
+
+ test "equality with simple types works" do
+ assert(@params != 'Hello')
+ assert(@params != 42)
+ assert(@params != false)
+ end
+
+ test "inspect shows both class name, parameters and permitted flag" do
+ assert_equal(
+ '<ActionController::Parameters {"person"=>{"age"=>"32", '\
+ '"name"=>{"first"=>"David", "last"=>"Heinemeier Hansson"}, ' \
+ '"addresses"=>[{"city"=>"Chicago", "state"=>"Illinois"}]}} permitted: false>',
+ @params.inspect
+ )
+ end
+
+ test "inspect prints updated permitted flag in the output" do
+ assert_match(/permitted: false/, @params.inspect)
+
+ @params.permit!
+
+ assert_match(/permitted: true/, @params.inspect)
+ end
+
+ if Hash.method_defined?(:dig)
+ test "#dig delegates the dig method to its values" do
+ assert_equal "David", @params.dig(:person, :name, :first)
+ assert_equal "Chicago", @params.dig(:person, :addresses, 0, :city)
+ end
+
+ test "#dig converts hashes to parameters" do
+ assert_kind_of ActionController::Parameters, @params.dig(:person)
+ assert_kind_of ActionController::Parameters, @params.dig(:person, :addresses, 0)
+ assert @params.dig(:person, :addresses).all? do |value|
+ value.is_a?(ActionController::Parameters)
+ end
+ end
+ else
+ test "ActionController::Parameters does not respond to #dig on Ruby 2.2" do
+ assert_not ActionController::Parameters.method_defined?(:dig)
+ assert_not @params.respond_to?(:dig)
+ end
end
end
diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
index efaf8a96c3..c5bfb10b53 100644
--- a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
+++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
@@ -12,12 +12,6 @@ class AlwaysPermittedParametersTest < ActiveSupport::TestCase
ActionController::Parameters.always_permitted_parameters = %w( controller action )
end
- test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do
- assert_deprecated do
- ActionController::Parameters::NEVER_UNPERMITTED_PARAMS
- end
- end
-
test "returns super on missing constant other than NEVER_UNPERMITTED_PARAMS" do
ActionController::Parameters.superclass.stub :const_missing, "super" do
assert_equal "super", ActionController::Parameters::NON_EXISTING_CONSTANT
diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
index 3299f2d9d0..b75eb0e3bf 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -27,6 +27,27 @@ class ParametersPermitTest < ActiveSupport::TestCase
end
end
+ def walk_permitted params
+ params.each do |k,v|
+ case v
+ when ActionController::Parameters
+ walk_permitted v
+ when Array
+ v.each { |x| walk_permitted v }
+ end
+ end
+ end
+
+ test 'iteration should not impact permit' do
+ hash = {"foo"=>{"bar"=>{"0"=>{"baz"=>"hello", "zot"=>"1"}}}}
+ params = ActionController::Parameters.new(hash)
+
+ walk_permitted params
+
+ sanitized = params[:foo].permit(bar: [:baz])
+ assert_equal({"0"=>{"baz"=>"hello"}}, sanitized[:bar].to_unsafe_h)
+ end
+
test 'if nothing is permitted, the hash becomes empty' do
params = ActionController::Parameters.new(id: '1234')
permitted = params.permit
@@ -339,4 +360,13 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert @params.include? 'person'
assert_not @params.include? :gorilla
end
+
+ test "scalar values should be filtered when array or hash is specified" do
+ params = ActionController::Parameters.new(foo: "bar")
+
+ assert params.permit(:foo).has_key?(:foo)
+ refute params.permit(foo: []).has_key?(:foo)
+ refute params.permit(foo: [:bar]).has_key?(:foo)
+ refute params.permit(foo: :bar).has_key?(:foo)
+ end
end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 0b184eace9..e10d4449f3 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -286,7 +286,10 @@ class RedirectTest < ActionController::TestCase
with_routing do |set|
set.draw do
resources :workshops
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
get :redirect_to_existing_record
@@ -310,7 +313,7 @@ class RedirectTest < ActionController::TestCase
error = assert_raise(ArgumentError) do
get :redirect_to_params
end
- assert_equal "Generating an URL from non sanitized request parameters is insecure!", error.message
+ assert_equal "Generating a URL from non sanitized request parameters is insecure!", error.message
end
def test_redirect_to_with_block
@@ -328,7 +331,9 @@ class RedirectTest < ActionController::TestCase
def test_redirect_to_with_block_and_accepted_options
with_routing do |set|
set.draw do
- get ':controller/:action'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
get :redirect_to_with_block_and_options
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index c814d4ea54..f42efd35af 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -16,6 +16,10 @@ class TestControllerWithExtraEtags < ActionController::Base
render plain: "stale" if stale?(etag: %w(1 2 3), template: false)
end
+ def strong
+ render plain: "stale" if stale?(strong_etag: 'strong')
+ end
+
def with_template
if stale? template: 'test/hello_world'
render plain: 'stale'
@@ -26,6 +30,9 @@ end
class ImplicitRenderTestController < ActionController::Base
def empty_action
end
+
+ def empty_action_with_template
+ end
end
class TestController < ActionController::Base
@@ -382,7 +389,7 @@ class LastModifiedRenderTest < ActionController::TestCase
def test_request_not_modified_but_etag_differs
@request.if_modified_since = @last_modified
- @request.if_none_match = "234"
+ @request.if_none_match = '"234"'
get :conditional_hello
assert_response :success
end
@@ -411,7 +418,7 @@ class LastModifiedRenderTest < ActionController::TestCase
def test_request_not_modified_but_etag_differs_with_record
@request.if_modified_since = @last_modified
- @request.if_none_match = "234"
+ @request.if_none_match = '"234"'
get :conditional_hello_with_record
assert_response :success
end
@@ -439,7 +446,7 @@ class LastModifiedRenderTest < ActionController::TestCase
def test_request_not_modified_but_etag_differs_with_collection_of_records
@request.if_modified_since = @last_modified
- @request.if_none_match = "234"
+ @request.if_none_match = '"234"'
get :conditional_hello_with_collection_of_records
assert_response :success
end
@@ -474,8 +481,26 @@ end
class EtagRenderTest < ActionController::TestCase
tests TestControllerWithExtraEtags
+ def test_strong_etag
+ @request.if_none_match = strong_etag(['strong', 'ab', :cde, [:f]])
+ get :strong
+ assert_response :not_modified
+
+ @request.if_none_match = '*'
+ get :strong
+ assert_response :not_modified
+
+ @request.if_none_match = '"strong"'
+ get :strong
+ assert_response :ok
+
+ @request.if_none_match = weak_etag(['strong', 'ab', :cde, [:f]])
+ get :strong
+ assert_response :ok
+ end
+
def test_multiple_etags
- @request.if_none_match = etag(["123", 'ab', :cde, [:f]])
+ @request.if_none_match = weak_etag(["123", 'ab', :cde, [:f]])
get :fresh
assert_response :not_modified
@@ -485,7 +510,7 @@ class EtagRenderTest < ActionController::TestCase
end
def test_array
- @request.if_none_match = etag([%w(1 2 3), 'ab', :cde, [:f]])
+ @request.if_none_match = weak_etag([%w(1 2 3), 'ab', :cde, [:f]])
get :array
assert_response :not_modified
@@ -509,7 +534,7 @@ class EtagRenderTest < ActionController::TestCase
begin
File.write path, 'foo'
- ActionView::Digestor.cache.clear
+ ActionView::LookupContext::DetailsKey.clear
request.if_none_match = etag
get :with_template
@@ -520,9 +545,14 @@ class EtagRenderTest < ActionController::TestCase
end
end
- def etag(record)
- %(W/"#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
- end
+ private
+ def weak_etag(record)
+ "W/#{strong_etag record}"
+ end
+
+ def strong_etag(record)
+ %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}")
+ end
end
class MetalRenderTest < ActionController::TestCase
@@ -537,10 +567,28 @@ end
class ImplicitRenderTest < ActionController::TestCase
tests ImplicitRenderTestController
- def test_implicit_no_content_response
- get :empty_action
+ def test_implicit_no_content_response_as_browser
+ assert_raises(ActionController::UnknownFormat) do
+ get :empty_action
+ end
+ end
+
+ def test_implicit_no_content_response_as_xhr
+ get :empty_action, xhr: true
assert_response :no_content
end
+
+ def test_implicit_success_response_with_right_format
+ get :empty_action_with_template
+ assert_equal "<h1>Empty action rendered this implicitly.</h1>\n", @response.body
+ assert_response :success
+ end
+
+ def test_implicit_unknown_format_response
+ assert_raises(ActionController::UnknownFormat) do
+ get :empty_action_with_template, format: 'json'
+ end
+ end
end
class HeadRenderTest < ActionController::TestCase
@@ -594,7 +642,10 @@ class HeadRenderTest < ActionController::TestCase
with_routing do |set|
set.draw do
resources :customers
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
get :head_with_location_object
@@ -679,7 +730,7 @@ end
class HttpCacheForeverTest < ActionController::TestCase
class HttpCacheForeverController < ActionController::Base
def cache_me_forever
- http_cache_forever(public: params[:public], version: params[:version] || 'v1') do
+ http_cache_forever(public: params[:public]) do
render plain: 'hello'
end
end
@@ -689,20 +740,24 @@ class HttpCacheForeverTest < ActionController::TestCase
def test_cache_with_public
get :cache_me_forever, params: {public: true}
+ assert_response :ok
assert_equal "max-age=#{100.years}, public", @response.headers["Cache-Control"]
assert_not_nil @response.etag
+ assert @response.weak_etag?
end
def test_cache_with_private
get :cache_me_forever
+ assert_response :ok
assert_equal "max-age=#{100.years}, private", @response.headers["Cache-Control"]
assert_not_nil @response.etag
- assert_response :success
+ assert @response.weak_etag?
end
def test_cache_response_code_with_if_modified_since
get :cache_me_forever
- assert_response :success
+ assert_response :ok
+
@request.if_modified_since = @response.headers['Last-Modified']
get :cache_me_forever
assert_response :not_modified
@@ -710,21 +765,10 @@ class HttpCacheForeverTest < ActionController::TestCase
def test_cache_response_code_with_etag
get :cache_me_forever
- assert_response :success
- @request.if_modified_since = @response.headers['Last-Modified']
- @request.if_none_match = @response.etag
-
- get :cache_me_forever
- assert_response :not_modified
- @request.if_modified_since = @response.headers['Last-Modified']
- @request.if_none_match = @response.etag
+ assert_response :ok
- get :cache_me_forever, params: {version: 'v2'}
- assert_response :success
- @request.if_modified_since = @response.headers['Last-Modified']
@request.if_none_match = @response.etag
-
- get :cache_me_forever, params: {version: 'v2'}
+ get :cache_me_forever
assert_response :not_modified
end
end
diff --git a/actionpack/test/controller/render_xml_test.rb b/actionpack/test/controller/render_xml_test.rb
index f0fd7ddc5e..137236c496 100644
--- a/actionpack/test/controller/render_xml_test.rb
+++ b/actionpack/test/controller/render_xml_test.rb
@@ -72,7 +72,10 @@ class RenderXmlTest < ActionController::TestCase
with_routing do |set|
set.draw do
resources :customers
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
get :render_with_object_location
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 1984ad8825..f7dcbc1984 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -133,7 +133,11 @@ class PerFormTokensController < ActionController::Base
self.per_form_csrf_tokens = true
def index
- render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: (params[:form_method] || :post) %>"
+ render inline: "<%= form_tag (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>"
+ end
+
+ def button_to
+ render inline: "<%= button_to 'Button', (params[:form_path] || '/per_form_tokens/post_one'), method: params[:form_method] %>"
end
def post_one
@@ -652,15 +656,9 @@ class PerFormTokensControllerTest < ActionController::TestCase
def test_accepts_token_for_correct_path_and_method
get :index
- form_token = nil
- assert_select 'input[name=custom_authenticity_token]' do |elts|
- form_token = elts.first['value']
- assert_not_nil form_token
- end
+ form_token = assert_presence_and_fetch_form_csrf_token
- actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
- expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post')
- assert_equal expected, actual
+ assert_matches_session_token_on_server form_token
# This is required because PATH_INFO isn't reset between requests.
@request.env['PATH_INFO'] = '/per_form_tokens/post_one'
@@ -673,15 +671,9 @@ class PerFormTokensControllerTest < ActionController::TestCase
def test_rejects_token_for_incorrect_path
get :index
- form_token = nil
- assert_select 'input[name=custom_authenticity_token]' do |elts|
- form_token = elts.first['value']
- assert_not_nil form_token
- end
+ form_token = assert_presence_and_fetch_form_csrf_token
- actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
- expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post')
- assert_equal expected, actual
+ assert_matches_session_token_on_server form_token
# This is required because PATH_INFO isn't reset between requests.
@request.env['PATH_INFO'] = '/per_form_tokens/post_two'
@@ -693,15 +685,9 @@ class PerFormTokensControllerTest < ActionController::TestCase
def test_rejects_token_for_incorrect_method
get :index
- form_token = nil
- assert_select 'input[name=custom_authenticity_token]' do |elts|
- form_token = elts.first['value']
- assert_not_nil form_token
- end
+ form_token = assert_presence_and_fetch_form_csrf_token
- actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
- expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post')
- assert_equal expected, actual
+ assert_matches_session_token_on_server form_token
# This is required because PATH_INFO isn't reset between requests.
@request.env['PATH_INFO'] = '/per_form_tokens/post_one'
@@ -710,6 +696,50 @@ class PerFormTokensControllerTest < ActionController::TestCase
end
end
+ def test_rejects_token_for_incorrect_method_button_to
+ get :button_to, params: { form_method: 'delete' }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, 'delete'
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
+ assert_raises(ActionController::InvalidAuthenticityToken) do
+ patch :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ test "Accepts proper token for implicit post method on button_to tag" do
+ get :button_to
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, 'post'
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
+ assert_nothing_raised do
+ post :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+
+ %w{delete post patch}.each do |verb|
+ test "Accepts proper token for #{verb} method on button_to tag" do
+ get :button_to, params: { form_method: verb }
+
+ form_token = assert_presence_and_fetch_form_csrf_token
+
+ assert_matches_session_token_on_server form_token, verb
+
+ # This is required because PATH_INFO isn't reset between requests.
+ @request.env['PATH_INFO'] = '/per_form_tokens/post_one'
+ assert_nothing_raised do
+ send verb, :post_one, params: { custom_authenticity_token: form_token }
+ end
+ end
+ end
+
def test_accepts_global_csrf_token
get :index
@@ -726,15 +756,9 @@ class PerFormTokensControllerTest < ActionController::TestCase
def test_ignores_params
get :index, params: {form_path: '/per_form_tokens/post_one?foo=bar'}
- form_token = nil
- assert_select 'input[name=custom_authenticity_token]' do |elts|
- form_token = elts.first['value']
- assert_not_nil form_token
- end
+ form_token = assert_presence_and_fetch_form_csrf_token
- actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
- expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', 'post')
- assert_equal expected, actual
+ assert_matches_session_token_on_server form_token
# This is required because PATH_INFO isn't reset between requests.
@request.env['PATH_INFO'] = '/per_form_tokens/post_one?foo=baz'
@@ -747,11 +771,7 @@ class PerFormTokensControllerTest < ActionController::TestCase
def test_ignores_trailing_slash_during_generation
get :index, params: {form_path: '/per_form_tokens/post_one/'}
- form_token = nil
- assert_select 'input[name=custom_authenticity_token]' do |elts|
- form_token = elts.first['value']
- assert_not_nil form_token
- end
+ form_token = assert_presence_and_fetch_form_csrf_token
# This is required because PATH_INFO isn't reset between requests.
@request.env['PATH_INFO'] = '/per_form_tokens/post_one'
@@ -764,11 +784,7 @@ class PerFormTokensControllerTest < ActionController::TestCase
def test_ignores_trailing_slash_during_validation
get :index
- form_token = nil
- assert_select 'input[name=custom_authenticity_token]' do |elts|
- form_token = elts.first['value']
- assert_not_nil form_token
- end
+ form_token = assert_presence_and_fetch_form_csrf_token
# This is required because PATH_INFO isn't reset between requests.
@request.env['PATH_INFO'] = '/per_form_tokens/post_one/'
@@ -781,12 +797,7 @@ class PerFormTokensControllerTest < ActionController::TestCase
def test_method_is_case_insensitive
get :index, params: {form_method: "POST"}
- form_token = nil
- assert_select 'input[name=custom_authenticity_token]' do |elts|
- form_token = elts.first['value']
- assert_not_nil form_token
- end
-
+ form_token = assert_presence_and_fetch_form_csrf_token
# This is required because PATH_INFO isn't reset between requests.
@request.env['PATH_INFO'] = '/per_form_tokens/post_one/'
assert_nothing_raised do
@@ -794,4 +805,19 @@ class PerFormTokensControllerTest < ActionController::TestCase
end
assert_response :success
end
+
+ private
+ def assert_presence_and_fetch_form_csrf_token
+ assert_select 'input[name="custom_authenticity_token"]' do |input|
+ form_csrf_token = input.first['value']
+ assert_not_nil form_csrf_token
+ return form_csrf_token
+ end
+ end
+
+ def assert_matches_session_token_on_server(form_token, method = 'post')
+ actual = @controller.send(:unmask_token, Base64.strict_decode64(form_token))
+ expected = @controller.send(:per_form_csrf_token, session, '/per_form_tokens/post_one', method)
+ assert_equal expected, actual
+ end
end
diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb
index f42bef883f..ed78f859ce 100644
--- a/actionpack/test/controller/rescue_test.rb
+++ b/actionpack/test/controller/rescue_test.rb
@@ -147,6 +147,24 @@ class RescueController < ActionController::Base
end
end
+ def exception_with_more_specific_handler_for_wrapper
+ raise RecordInvalid
+ rescue
+ raise NotAuthorized
+ end
+
+ def exception_with_more_specific_handler_for_cause
+ raise NotAuthorized
+ rescue
+ raise RecordInvalid
+ end
+
+ def exception_with_no_handler_for_wrapper
+ raise RecordInvalid
+ rescue
+ raise RangeError
+ end
+
protected
def deny_access
head :forbidden
@@ -301,6 +319,21 @@ class RescueControllerTest < ActionController::TestCase
get :resource_unavailable_raise_as_string
assert_equal "RescueController::ResourceUnavailableToRescueAsString", @response.body
end
+
+ test 'rescue when wrapper has more specific handler than cause' do
+ get :exception_with_more_specific_handler_for_wrapper
+ assert_response :unprocessable_entity
+ end
+
+ test 'rescue when cause has more specific handler than wrapper' do
+ get :exception_with_more_specific_handler_for_cause
+ assert_response :unprocessable_entity
+ end
+
+ test 'rescue when cause has handler, but wrapper doesnt' do
+ get :exception_with_no_handler_for_wrapper
+ assert_response :unprocessable_entity
+ end
end
class RescueTest < ActionDispatch::IntegrationTest
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index a39fede5b9..c477b4156c 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -15,7 +15,9 @@ class UriReservedCharactersRoutingTest < ActiveSupport::TestCase
def setup
@set = ActionDispatch::Routing::RouteSet.new
@set.draw do
- get ':controller/:action/:variable/*additional'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:variable/*additional'
+ end
end
safe, unsafe = %w(: @ & = + $ , ;), %w(^ ? # [ ])
@@ -300,7 +302,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
end
def test_default_setup
- rs.draw { get '/:controller(/:action(/:id))' }
+ rs.draw { ActiveSupport::Deprecation.silence { get '/:controller(/:action(/:id))' } }
assert_equal({:controller => "content", :action => 'index'}, rs.recognize_path("/content"))
assert_equal({:controller => "content", :action => 'list'}, rs.recognize_path("/content/list"))
assert_equal({:controller => "content", :action => 'show', :id => '10'}, rs.recognize_path("/content/show/10"))
@@ -323,7 +325,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_route_with_colon_first
rs.draw do
- get '/:controller/:action/:id', action: 'index', id: nil
+ ActiveSupport::Deprecation.silence do
+ get '/:controller/:action/:id', action: 'index', id: nil
+ end
+
get ':url', controller: 'content', action: 'translate'
end
@@ -331,7 +336,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
end
def test_route_with_regexp_for_action
- rs.draw { get '/:controller/:action', action: /auth[-|_].+/ }
+ rs.draw { ActiveSupport::Deprecation.silence { get '/:controller/:action', action: /auth[-|_].+/ } }
assert_equal({ action: 'auth_google', controller: 'content' }, rs.recognize_path('/content/auth_google'))
assert_equal({ action: 'auth-facebook', controller: 'content' }, rs.recognize_path('/content/auth-facebook'))
@@ -342,8 +347,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_route_with_regexp_for_controller
rs.draw do
- get ':controller/:admintoken(/:action(/:id))', :controller => /admin\/.+/
- get '/:controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:admintoken(/:action(/:id))', :controller => /admin\/.+/
+ get '/:controller(/:action(/:id))'
+ end
end
assert_equal({:controller => "admin/user", :admintoken => "foo", :action => "index"},
@@ -357,7 +364,9 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_route_with_regexp_and_captures_for_controller
rs.draw do
- get '/:controller(/:action(/:id))', :controller => /admin\/(accounts|users)/
+ ActiveSupport::Deprecation.silence do
+ get '/:controller(/:action(/:id))', :controller => /admin\/(accounts|users)/
+ end
end
assert_equal({:controller => "admin/accounts", :action => "index"}, rs.recognize_path("/admin/accounts"))
assert_equal({:controller => "admin/users", :action => "index"}, rs.recognize_path("/admin/users"))
@@ -366,11 +375,13 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_route_with_regexp_and_dot
rs.draw do
- get ':controller/:action/:file',
- :controller => /admin|user/,
- :action => /upload|download/,
- :defaults => {:file => nil},
- :constraints => {:file => %r{[^/]+(\.[^/]+)?}}
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:file',
+ :controller => /admin|user/,
+ :action => /upload|download/,
+ :defaults => {:file => nil},
+ :constraints => {:file => %r{[^/]+(\.[^/]+)?}}
+ end
end
# Without a file extension
assert_equal '/user/download/file',
@@ -457,7 +468,9 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_named_route_without_hash
rs.draw do
- get ':controller/:action/:id', :as => 'normal'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id', :as => 'normal'
+ end
end
end
@@ -509,7 +522,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
rs.draw do
get 'page/:year/:month/:day/:title' => 'page#show', :as => 'article',
:year => /\d+/, :month => /\d+/, :day => /\d+/
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
routes = setup_for_named_route
@@ -519,7 +535,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
end
def test_changing_controller
- rs.draw { get ':controller/:action/:id' }
+ rs.draw { ActiveSupport::Deprecation.silence { get ':controller/:action/:id' } }
get URI('http://test.host/admin/user/index/10')
@@ -530,7 +546,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_paths_escaped
rs.draw do
get 'file/*path' => 'content#show_file', :as => 'path'
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
# No + to space in URI escaping, only for query params.
@@ -555,7 +574,9 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_non_controllers_cannot_be_matched
rs.draw do
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
assert_raise(ActionController::RoutingError) { rs.recognize_path("/not_a/show/10") }
end
@@ -593,8 +614,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_backwards
rs.draw do
- get 'page/:id(/:action)' => 'pages#show'
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get 'page/:id(/:action)' => 'pages#show'
+ get ':controller(/:action(/:id))'
+ end
end
get URI('http://test.host/pages/show')
@@ -606,7 +629,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_route_with_fixnum_default
rs.draw do
get 'page(/:id)' => 'content#show_page', :id => 1
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
assert_equal '/page', url_for(rs, { :controller => 'content', :action => 'show_page' })
@@ -623,7 +649,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_route_with_text_default
rs.draw do
get 'page/:id' => 'content#show_page', :id => 1
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
assert_equal '/page/foo', url_for(rs, { :controller => 'content', :action => 'show_page', :id => 'foo' })
@@ -638,7 +667,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
end
def test_action_expiry
- rs.draw { get ':controller(/:action(/:id))' }
+ rs.draw { ActiveSupport::Deprecation.silence { get ':controller(/:action(/:id))' } }
get URI('http://test.host/content/show')
assert_equal '/content', controller.url_for(:controller => 'content', :only_path => true)
end
@@ -661,7 +690,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
:defaults => { :year => nil },
:constraints => { :year => /\d{4}/ }
)
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
assert_equal '/test', url_for(rs, { :controller => 'post', :action => 'show' })
@@ -673,7 +705,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_set_to_nil_forgets
rs.draw do
get 'pages(/:year(/:month(/:day)))' => 'content#list_pages', :month => nil, :day => nil
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
assert_equal '/pages/2005',
@@ -720,7 +755,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_named_route_method
rs.draw do
get 'categories' => 'content#categories', :as => 'categories'
- get ':controller(/:action(/:id))'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
assert_equal '/categories', url_for(rs, { :controller => 'content', :action => 'categories' })
@@ -736,7 +774,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
rs.draw do
get 'journal' => 'content#list_journal',
:date => nil, :user_id => nil
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
assert_equal '/journal', url_for(rs, {
@@ -776,10 +817,12 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_subpath_recognized
rs.draw do
- get '/books/:id/edit' => 'subpath_books#edit'
- get '/items/:id/:action' => 'subpath_books'
- get '/posts/new/:action' => 'subpath_books'
- get '/posts/:id' => 'subpath_books#show'
+ ActiveSupport::Deprecation.silence do
+ get '/books/:id/edit' => 'subpath_books#edit'
+ get '/items/:id/:action' => 'subpath_books'
+ get '/posts/new/:action' => 'subpath_books'
+ get '/posts/:id' => 'subpath_books#show'
+ end
end
hash = rs.recognize_path "/books/17/edit"
@@ -801,9 +844,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_subpath_generated
rs.draw do
- get '/books/:id/edit' => 'subpath_books#edit'
- get '/items/:id/:action' => 'subpath_books'
- get '/posts/new/:action' => 'subpath_books'
+ ActiveSupport::Deprecation.silence do
+ get '/books/:id/edit' => 'subpath_books#edit'
+ get '/items/:id/:action' => 'subpath_books'
+ get '/posts/new/:action' => 'subpath_books'
+ end
end
assert_equal "/books/7/edit", url_for(rs, { :controller => "subpath_books", :id => 7, :action => "edit" })
@@ -827,8 +872,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
get 'ca' => 'ca#aa'
get 'cb' => 'cb#ab'
get 'cc' => 'cc#ac'
- get ':controller/:action/:id'
- get ':controller/:action/:id.:format'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ get ':controller/:action/:id.:format'
+ end
end
hash = rs.recognize_path "/cc"
@@ -839,8 +887,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
rs.draw do
get 'cb' => 'cb#ab'
get 'cc' => 'cc#ac'
- get ':controller/:action/:id'
- get ':controller/:action/:id.:format'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ get ':controller/:action/:id.:format'
+ end
end
hash = rs.recognize_path "/cc"
@@ -871,29 +922,34 @@ class RouteSetTest < ActiveSupport::TestCase
@default_route_set ||= begin
set = ActionDispatch::Routing::RouteSet.new
set.draw do
- get '/:controller(/:action(/:id))'
+
+ ActiveSupport::Deprecation.silence do
+ get '/:controller(/:action(/:id))'
+ end
end
set
end
end
def test_generate_extras
- set.draw { get ':controller/(:action(/:id))' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller/(:action(/:id))' } }
path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
assert_equal "/foo/bar/15", path
assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_extra_keys
- set.draw { get ':controller/:action/:id' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller/:action/:id' } }
extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_generate_extras_not_first
set.draw do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id.:format'
+ get ':controller/:action/:id'
+ end
end
path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
assert_equal "/foo/bar/15", path
@@ -902,8 +958,10 @@ class RouteSetTest < ActiveSupport::TestCase
def test_generate_not_first
set.draw do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id.:format'
+ get ':controller/:action/:id'
+ end
end
assert_equal "/foo/bar/15?this=hello",
url_for(set, { :controller => "foo", :action => "bar", :id => 15, :this => "hello" })
@@ -911,8 +969,10 @@ class RouteSetTest < ActiveSupport::TestCase
def test_extra_keys_not_first
set.draw do
- get ':controller/:action/:id.:format'
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id.:format'
+ get ':controller/:action/:id'
+ end
end
extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
assert_equal %w(that this), extras.map(&:to_s).sort
@@ -1044,7 +1104,9 @@ class RouteSetTest < ActiveSupport::TestCase
def test_draw_default_route
set.draw do
- get '/:controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
assert_equal 1, set.routes.size
@@ -1059,7 +1121,10 @@ class RouteSetTest < ActiveSupport::TestCase
def test_route_with_parameter_shell
set.draw do
get 'page/:id' => 'pages#show', :id => /\d+/
- get '/:controller(/:action(/:id))'
+
+ ActiveSupport::Deprecation.silence do
+ get '/:controller(/:action(/:id))'
+ end
end
assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages'))
@@ -1314,7 +1379,9 @@ class RouteSetTest < ActiveSupport::TestCase
@set = make_set false
set.draw do
- get ':controller/:id/:action'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:id/:action'
+ end
end
get URI('http://test.host/people/7/show')
@@ -1327,7 +1394,10 @@ class RouteSetTest < ActiveSupport::TestCase
set.draw do
get 'about' => "welcome#about"
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:id/:action'
+ end
end
get URI('http://test.host/welcom/get/7')
@@ -1338,7 +1408,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
def test_generate
- set.draw { get ':controller/:action/:id' }
+ set.draw { ActiveSupport::Deprecation.silence { get ':controller/:action/:id' } }
args = { :controller => "foo", :action => "bar", :id => "7", :x => "y" }
assert_equal "/foo/bar/7?x=y", url_for(set, args)
@@ -1349,7 +1419,9 @@ class RouteSetTest < ActiveSupport::TestCase
def test_generate_with_path_prefix
set.draw do
scope "my" do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
end
@@ -1360,7 +1432,9 @@ class RouteSetTest < ActiveSupport::TestCase
def test_generate_with_blank_path_prefix
set.draw do
scope "" do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
end
@@ -1372,9 +1446,11 @@ class RouteSetTest < ActiveSupport::TestCase
@set = make_set false
set.draw do
- get "/connection/manage(/:action)" => 'connection/manage#index'
- get "/connection/connection" => "connection/connection#index"
- get '/connection' => 'connection#index', :as => 'family_connection'
+ ActiveSupport::Deprecation.silence do
+ get "/connection/manage(/:action)" => 'connection/manage#index'
+ get "/connection/connection" => "connection/connection#index"
+ get '/connection' => 'connection#index', :as => 'family_connection'
+ end
end
assert_equal({ :controller => 'connection/manage',
@@ -1392,7 +1468,9 @@ class RouteSetTest < ActiveSupport::TestCase
@set = make_set false
set.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
get URI('http://test.host/books/show/10')
@@ -1407,7 +1485,10 @@ class RouteSetTest < ActiveSupport::TestCase
set.draw do
get 'show_weblog/:parameter' => 'weblog#show'
- get ':controller(/:action(/:id))'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
get URI('http://test.host/weblog/show/1')
@@ -1435,7 +1516,7 @@ class RouteSetTest < ActiveSupport::TestCase
def test_expiry_determination_should_consider_values_with_to_param
@set = make_set false
- set.draw { get 'projects/:project_id/:controller/:action' }
+ set.draw { ActiveSupport::Deprecation.silence { get 'projects/:project_id/:controller/:action' } }
get URI('http://test.host/projects/1/weblog/show')
@@ -1612,7 +1693,9 @@ class RouteSetTest < ActiveSupport::TestCase
def test_assign_route_options_with_anchor_chars
set.draw do
- get '/cars/:action/:person/:car/', :controller => 'cars'
+ ActiveSupport::Deprecation.silence do
+ get '/cars/:action/:person/:car/', :controller => 'cars'
+ end
end
assert_equal '/cars/buy/1/2', url_for(set, { :controller => 'cars', :action => 'buy', :person => '1', :car => '2' })
@@ -1622,7 +1705,9 @@ class RouteSetTest < ActiveSupport::TestCase
def test_segmentation_of_dot_path
set.draw do
- get '/books/:action.rss', :controller => 'books'
+ ActiveSupport::Deprecation.silence do
+ get '/books/:action.rss', :controller => 'books'
+ end
end
assert_equal '/books/list.rss', url_for(set, { :controller => 'books', :action => 'list' })
@@ -1632,7 +1717,9 @@ class RouteSetTest < ActiveSupport::TestCase
def test_segmentation_of_dynamic_dot_path
set.draw do
- get '/books(/:action(.:format))', :controller => 'books'
+ ActiveSupport::Deprecation.silence do
+ get '/books(/:action(.:format))', :controller => 'books'
+ end
end
assert_equal '/books/list.rss', url_for(set, { :controller => 'books', :action => 'list', :format => 'rss' })
@@ -1647,7 +1734,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
def test_slashes_are_implied
- set.draw { get("/:controller(/:action(/:id))") }
+ set.draw { ActiveSupport::Deprecation.silence { get("/:controller(/:action(/:id))") } }
assert_equal '/content', url_for(set, { :controller => 'content', :action => 'index' })
assert_equal '/content/list', url_for(set, { :controller => 'content', :action => 'list' })
@@ -1738,7 +1825,9 @@ class RouteSetTest < ActiveSupport::TestCase
:constraints => { :page => /\d+/ },
:defaults => { :page => 1 }
- get ':controller/:action/:id'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
assert_equal '/ibocorp', url_for(set, { :controller => 'ibocorp', :action => "show", :page => 1 })
@@ -1761,7 +1850,11 @@ class RouteSetTest < ActiveSupport::TestCase
:day => nil, :month => nil
get "blog/show/:id", :controller => "blog", :action => "show", :id => /\d+/
- get "blog/:controller/:action(/:id)"
+
+ ActiveSupport::Deprecation.silence do
+ get "blog/:controller/:action(/:id)"
+ end
+
get "*anything", :controller => "blog", :action => "unknown_request"
end
@@ -1850,13 +1943,20 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
get 'news(.:format)' => "news#index"
- get 'comment/:id(/:action)' => "comments#show"
- get 'ws/:controller(/:action(/:id))', :ws => true
- get 'account(/:action)' => "account#subscription"
- get 'pages/:page_id/:controller(/:action(/:id))'
- get ':controller/ping', :action => 'ping'
+ ActiveSupport::Deprecation.silence do
+ get 'comment/:id(/:action)' => "comments#show"
+ get 'ws/:controller(/:action(/:id))', :ws => true
+ get 'account(/:action)' => "account#subscription"
+ get 'pages/:page_id/:controller(/:action(/:id))'
+ get ':controller/ping', :action => 'ping'
+ end
+
get 'こんにちは/世界', :controller => 'news', :action => 'index'
- match ':controller(/:action(/:id))(.:format)', :via => :all
+
+ ActiveSupport::Deprecation.silence do
+ match ':controller(/:action(/:id))(.:format)', :via => :all
+ end
+
root :to => "news#index"
}
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index b9caddcdb7..ebcdda6074 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -137,6 +137,10 @@ XML
head :created, location: 'created resource'
end
+ def render_cookie
+ render plain: cookies["foo"]
+ end
+
def delete_cookie
cookies.delete("foo")
render plain: 'ok'
@@ -163,7 +167,9 @@ XML
@request.delete_header 'PATH_INFO'
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
end
end
@@ -668,7 +674,10 @@ XML
with_routing do |set|
set.draw do
get 'file/*path', to: 'test_case_test/test#test_params'
- get ':controller/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
get :test_params, params: { path: ['hello', 'world'] }
@@ -829,6 +838,12 @@ XML
assert_equal 'bar', cookies['foo']
end
+ def test_cookies_should_be_escaped_properly
+ cookies['foo'] = '+'
+ get :render_cookie
+ assert_equal '+', @response.body
+ end
+
def test_should_detect_if_cookie_is_deleted
cookies['foo'] = 'bar'
get :delete_cookie
@@ -998,7 +1013,9 @@ class ResponseDefaultHeadersTest < ActionController::TestCase
@request.env['PATH_INFO'] = nil
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
end
end
@@ -1125,7 +1142,9 @@ class AnonymousControllerTest < ActionController::TestCase
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
end
end
diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb
index dfc2712e3e..a6ca5fc868 100644
--- a/actionpack/test/controller/url_for_integration_test.rb
+++ b/actionpack/test/controller/url_for_integration_test.rb
@@ -52,12 +52,15 @@ module ActionPack
get 'news(.:format)' => "news#index"
- get 'comment/:id(/:action)' => "comments#show"
- get 'ws/:controller(/:action(/:id))', :ws => true
- get 'account(/:action)' => "account#subscription"
- get 'pages/:page_id/:controller(/:action(/:id))'
- get ':controller/ping', :action => 'ping'
- get ':controller(/:action(/:id))(.:format)'
+ ActiveSupport::Deprecation.silence {
+ get 'comment/:id(/:action)' => "comments#show"
+ get 'ws/:controller(/:action(/:id))', :ws => true
+ get 'account(/:action)' => "account#subscription"
+ get 'pages/:page_id/:controller(/:action(/:id))'
+ get ':controller/ping', :action => 'ping'
+ get ':controller(/:action(/:id))(.:format)'
+ }
+
root :to => "news#index"
}
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index 67212fea38..b4d2088c0a 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -4,7 +4,13 @@ module AbstractController
module Testing
class UrlForTest < ActionController::TestCase
class W
- include ActionDispatch::Routing::RouteSet.new.tap { |r| r.draw { get ':controller(/:action(/:id(.:format)))' } }.url_helpers
+ include ActionDispatch::Routing::RouteSet.new.tap { |r|
+ r.draw {
+ ActiveSupport::Deprecation.silence {
+ get ':controller(/:action(/:id(.:format)))'
+ }
+ }
+ }.url_helpers
end
def teardown
@@ -260,7 +266,7 @@ module AbstractController
w = Class.new {
config = ActionDispatch::Routing::RouteSet::Config.new '/subdir'
r = ActionDispatch::Routing::RouteSet.new(config)
- r.draw { get ':controller(/:action(/:id(.:format)))' }
+ r.draw { ActiveSupport::Deprecation.silence { get ':controller(/:action(/:id(.:format)))' } }
include r.url_helpers
}
add_host!(w)
@@ -315,7 +321,10 @@ module AbstractController
with_routing do |set|
set.draw do
get 'home/sweet/home/:user', :to => 'home#index', :as => :home
- get ':controller/:action/:id'
+
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/:id'
+ end
end
# We need to create a new class in order to install the new named route.
diff --git a/actionpack/test/controller/url_rewriter_test.rb b/actionpack/test/controller/url_rewriter_test.rb
index 5f2abc9606..bc0d215530 100644
--- a/actionpack/test/controller/url_rewriter_test.rb
+++ b/actionpack/test/controller/url_rewriter_test.rb
@@ -20,7 +20,9 @@ class UrlRewriterTests < ActionController::TestCase
@rewriter = Rewriter.new(@request) #.new(@request, @params)
@routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
r.draw do
- get ':controller(/:action(/:id))'
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))'
+ end
end
end
end
diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
index 6d377c4691..daf17558aa 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_test.rb
@@ -99,7 +99,7 @@ class WebServiceTest < ActionDispatch::IntegrationTest
def test_parsing_json_doesnot_rescue_exception
req = Class.new(ActionDispatch::Request) do
def params_parsers
- { Mime[:json] => Proc.new { |data| raise Interrupt } }
+ { json: Proc.new { |data| raise Interrupt } }
end
def content_length; get_header('rack.input').length; end
diff --git a/actionpack/test/dispatch/callbacks_test.rb b/actionpack/test/dispatch/callbacks_test.rb
index 5ba76d9ab9..7b707df7f6 100644
--- a/actionpack/test/dispatch/callbacks_test.rb
+++ b/actionpack/test/dispatch/callbacks_test.rb
@@ -37,13 +37,19 @@ class DispatcherTest < ActiveSupport::TestCase
def test_to_prepare_and_cleanup_delegation
prepared = cleaned = false
- ActionDispatch::Callbacks.to_prepare { prepared = true }
- ActionDispatch::Callbacks.to_prepare { cleaned = true }
+ assert_deprecated do
+ ActionDispatch::Callbacks.to_prepare { prepared = true }
+ ActionDispatch::Callbacks.to_prepare { cleaned = true }
+ end
- ActionDispatch::Reloader.prepare!
+ assert_deprecated do
+ ActionDispatch::Reloader.prepare!
+ end
assert prepared
- ActionDispatch::Reloader.cleanup!
+ assert_deprecated do
+ ActionDispatch::Reloader.cleanup!
+ end
assert cleaned
end
diff --git a/actionpack/test/dispatch/executor_test.rb b/actionpack/test/dispatch/executor_test.rb
new file mode 100644
index 0000000000..28bb232ecd
--- /dev/null
+++ b/actionpack/test/dispatch/executor_test.rb
@@ -0,0 +1,134 @@
+require 'abstract_unit'
+
+class ExecutorTest < ActiveSupport::TestCase
+ class MyBody < Array
+ def initialize(&block)
+ @on_close = block
+ end
+
+ def foo
+ "foo"
+ end
+
+ def bar
+ "bar"
+ end
+
+ def close
+ @on_close.call if @on_close
+ end
+ end
+
+ def test_returned_body_object_always_responds_to_close
+ body = call_and_return_body
+ assert_respond_to body, :close
+ end
+
+ def test_returned_body_object_always_responds_to_close_even_if_called_twice
+ body = call_and_return_body
+ assert_respond_to body, :close
+ body.close
+
+ body = call_and_return_body
+ assert_respond_to body, :close
+ body.close
+ end
+
+ def test_returned_body_object_behaves_like_underlying_object
+ body = call_and_return_body do
+ b = MyBody.new
+ b << "hello"
+ b << "world"
+ [200, { "Content-Type" => "text/html" }, b]
+ end
+ assert_equal 2, body.size
+ assert_equal "hello", body[0]
+ assert_equal "world", body[1]
+ assert_equal "foo", body.foo
+ assert_equal "bar", body.bar
+ end
+
+ def test_it_calls_close_on_underlying_object_when_close_is_called_on_body
+ close_called = false
+ body = call_and_return_body do
+ b = MyBody.new do
+ close_called = true
+ end
+ [200, { "Content-Type" => "text/html" }, b]
+ end
+ body.close
+ assert close_called
+ end
+
+ def test_returned_body_object_responds_to_all_methods_supported_by_underlying_object
+ body = call_and_return_body do
+ [200, { "Content-Type" => "text/html" }, MyBody.new]
+ end
+ assert_respond_to body, :size
+ assert_respond_to body, :each
+ assert_respond_to body, :foo
+ assert_respond_to body, :bar
+ end
+
+ def test_run_callbacks_are_called_before_close
+ running = false
+ executor.to_run { running = true }
+
+ body = call_and_return_body
+ assert running
+
+ running = false
+ body.close
+ assert !running
+ end
+
+ def test_complete_callbacks_are_called_on_close
+ completed = false
+ executor.to_complete { completed = true }
+
+ body = call_and_return_body
+ assert !completed
+
+ body.close
+ assert completed
+ end
+
+ def test_complete_callbacks_are_called_on_exceptions
+ completed = false
+ executor.to_complete { completed = true }
+
+ begin
+ call_and_return_body do
+ raise "error"
+ end
+ rescue
+ end
+
+ assert completed
+ end
+
+ def test_callbacks_execute_in_shared_context
+ result = false
+ executor.to_run { @in_shared_context = true }
+ executor.to_complete { result = @in_shared_context }
+
+ call_and_return_body.close
+ assert result
+ assert !defined?(@in_shared_context) # it's not in the test itself
+ end
+
+ private
+ def call_and_return_body(&block)
+ app = middleware(block || proc { [200, {}, 'response'] })
+ _, _, body = app.call({'rack.input' => StringIO.new('')})
+ body
+ end
+
+ def middleware(inner_app)
+ ActionDispatch::Executor.new(inner_app, executor)
+ end
+
+ def executor
+ @executor ||= Class.new(ActiveSupport::Executor)
+ end
+end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index 149e37bf3d..672b272590 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -49,7 +49,7 @@ class MimeTypeTest < ActiveSupport::TestCase
test "parse application with trailing star" do
accept = "application/*"
- expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip]]
+ expect = [Mime[:html], Mime[:js], Mime[:xml], Mime[:rss], Mime[:atom], Mime[:yaml], Mime[:url_encoded_form], Mime[:json], Mime[:pdf], Mime[:zip], Mime[:gzip]]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb
index 62e8197e20..fe8a4a3a17 100644
--- a/actionpack/test/dispatch/reloader_test.rb
+++ b/actionpack/test/dispatch/reloader_test.rb
@@ -4,15 +4,17 @@ class ReloaderTest < ActiveSupport::TestCase
Reloader = ActionDispatch::Reloader
teardown do
- Reloader.reset_callbacks :prepare
- Reloader.reset_callbacks :cleanup
+ ActiveSupport::Reloader.reset_callbacks :prepare
+ ActiveSupport::Reloader.reset_callbacks :complete
end
def test_prepare_callbacks
a = b = c = nil
- Reloader.to_prepare { |*args| a = b = c = 1 }
- Reloader.to_prepare { |*args| b = c = 2 }
- Reloader.to_prepare { |*args| c = 3 }
+ assert_deprecated do
+ Reloader.to_prepare { |*args| a = b = c = 1 }
+ Reloader.to_prepare { |*args| b = c = 2 }
+ Reloader.to_prepare { |*args| c = 3 }
+ end
# Ensure to_prepare callbacks are not run when defined
assert_nil a || b || c
@@ -60,9 +62,15 @@ class ReloaderTest < ActiveSupport::TestCase
def test_condition_specifies_when_to_reload
i, j = 0, 0, 0, 0
- Reloader.to_prepare { |*args| i += 1 }
- Reloader.to_cleanup { |*args| j += 1 }
- app = Reloader.new(lambda { |env| [200, {}, []] }, lambda { i < 3 })
+ assert_deprecated do
+ Reloader.to_prepare { |*args| i += 1 }
+ Reloader.to_cleanup { |*args| j += 1 }
+ end
+
+ x = Class.new(ActiveSupport::Reloader)
+ x.check = lambda { i < 3 }
+
+ app = Reloader.new(lambda { |env| [200, {}, []] }, x)
5.times do
resp = app.call({})
resp[2].close
@@ -109,7 +117,9 @@ class ReloaderTest < ActiveSupport::TestCase
def test_cleanup_callbacks_are_called_when_body_is_closed
cleaned = false
- Reloader.to_cleanup { cleaned = true }
+ assert_deprecated do
+ Reloader.to_cleanup { cleaned = true }
+ end
body = call_and_return_body
assert !cleaned
@@ -120,7 +130,9 @@ class ReloaderTest < ActiveSupport::TestCase
def test_prepare_callbacks_arent_called_when_body_is_closed
prepared = false
- Reloader.to_prepare { prepared = true }
+ assert_deprecated do
+ Reloader.to_prepare { prepared = true }
+ end
body = call_and_return_body
prepared = false
@@ -131,31 +143,43 @@ class ReloaderTest < ActiveSupport::TestCase
def test_manual_reloading
prepared = cleaned = false
- Reloader.to_prepare { prepared = true }
- Reloader.to_cleanup { cleaned = true }
+ assert_deprecated do
+ Reloader.to_prepare { prepared = true }
+ Reloader.to_cleanup { cleaned = true }
+ end
- Reloader.prepare!
+ assert_deprecated do
+ Reloader.prepare!
+ end
assert prepared
assert !cleaned
prepared = cleaned = false
- Reloader.cleanup!
- assert !prepared
+ assert_deprecated do
+ Reloader.cleanup!
+ end
+ assert prepared
assert cleaned
end
def test_prepend_prepare_callback
i = 10
- Reloader.to_prepare { i += 1 }
- Reloader.to_prepare(:prepend => true) { i = 0 }
+ assert_deprecated do
+ Reloader.to_prepare { i += 1 }
+ Reloader.to_prepare(:prepend => true) { i = 0 }
+ end
- Reloader.prepare!
+ assert_deprecated do
+ Reloader.prepare!
+ end
assert_equal 1, i
end
def test_cleanup_callbacks_are_called_on_exceptions
cleaned = false
- Reloader.to_cleanup { cleaned = true }
+ assert_deprecated do
+ Reloader.to_cleanup { cleaned = true }
+ end
begin
call_and_return_body do
@@ -169,8 +193,11 @@ class ReloaderTest < ActiveSupport::TestCase
private
def call_and_return_body(&block)
+ x = Class.new(ActiveSupport::Reloader)
+ x.check = lambda { true }
+
@response ||= 'response'
- @reloader ||= Reloader.new(block || proc {[200, {}, @response]})
+ @reloader ||= Reloader.new(block || proc {[200, {}, @response]}, x)
@reloader.call({'rack.input' => StringIO.new('')})[2]
end
end
diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb
index a3992ad008..a07138b55e 100644
--- a/actionpack/test/dispatch/request/json_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb
@@ -37,9 +37,9 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
)
end
- test "parses json params for application/vnd.api+json" do
+ test "does not parse unregistered media types such as application/vnd.api+json" do
assert_parses(
- {"person" => {"name" => "David"}},
+ {},
"{\"person\": {\"name\": \"David\"}}", { 'CONTENT_TYPE' => 'application/vnd.api+json' }
)
end
@@ -103,7 +103,9 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', :to => ::JsonParamsParsingTest::TestController
+ ActiveSupport::Deprecation.silence do
+ post ':action', :to => ::JsonParamsParsingTest::TestController
+ end
end
yield
end
@@ -143,13 +145,6 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
)
end
- test "parses json params for application/vnd.api+json" do
- assert_parses(
- {"user" => {"username" => "sikachu"}, "username" => "sikachu"},
- "{\"username\": \"sikachu\"}", { 'CONTENT_TYPE' => 'application/vnd.api+json' }
- )
- end
-
test "parses json with non-object JSON content" do
assert_parses(
{"user" => {"_json" => "string content" }, "_json" => "string content" },
@@ -157,6 +152,34 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
)
end
+ test "parses json params after custom json mime type registered" do
+ begin
+ Mime::Type.unregister :json
+ Mime::Type.register "application/json", :json, %w(application/vnd.api+json)
+ assert_parses(
+ {"user" => {"username" => "meinac"}, "username" => "meinac"},
+ "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/json' }
+ )
+ ensure
+ Mime::Type.unregister :json
+ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
+ end
+ end
+
+ test "parses json params after custom json mime type registered with synonym" do
+ begin
+ Mime::Type.unregister :json
+ Mime::Type.register "application/json", :json, %w(application/vnd.api+json)
+ assert_parses(
+ {"user" => {"username" => "meinac"}, "username" => "meinac"},
+ "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/vnd.api+json' }
+ )
+ ensure
+ Mime::Type.unregister :json
+ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonrequest )
+ end
+ end
+
private
def assert_parses(expected, actual, headers = {})
with_test_routing(UsersController) do
@@ -170,7 +193,9 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing(controller)
with_routing do |set|
set.draw do
- post ':action', :to => controller
+ ActiveSupport::Deprecation.silence do
+ post ':action', :to => controller
+ end
end
yield
end
diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
index b36fbd3c76..bab4413b2a 100644
--- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
@@ -159,7 +159,9 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
test "does not raise EOFError on GET request with multipart content-type" do
with_routing do |set|
set.draw do
- get ':action', controller: 'multipart_params_parsing_test/test'
+ ActiveSupport::Deprecation.silence do
+ get ':action', controller: 'multipart_params_parsing_test/test'
+ end
end
headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" }
get "/parse", headers: headers
@@ -188,7 +190,9 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', :controller => 'multipart_params_parsing_test/test'
+ ActiveSupport::Deprecation.silence do
+ post ':action', :controller => 'multipart_params_parsing_test/test'
+ end
end
yield
end
diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb
index bc6716525e..f04022a544 100644
--- a/actionpack/test/dispatch/request/query_string_parsing_test.rb
+++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb
@@ -144,7 +144,9 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
test "ambiguous query string returns a bad request" do
with_routing do |set|
set.draw do
- get ':action', :to => ::QueryStringParsingTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ':action', :to => ::QueryStringParsingTest::TestController
+ end
end
get "/parse", headers: { "QUERY_STRING" => "foo[]=bar&foo[4]=bar" }
@@ -156,7 +158,9 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
def assert_parses(expected, actual)
with_routing do |set|
set.draw do
- get ':action', :to => ::QueryStringParsingTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ':action', :to => ::QueryStringParsingTest::TestController
+ end
end
@app = self.class.build_app(set) do |middleware|
middleware.use(EarlyParse)
diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
index 365edf849a..b9f8c52378 100644
--- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
@@ -140,7 +140,9 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', to: ::UrlEncodedParamsParsingTest::TestController
+ ActiveSupport::Deprecation.silence do
+ post ':action', to: ::UrlEncodedParamsParsingTest::TestController
+ end
end
yield
end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index 0edad72fd9..a4cb8ce449 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -1152,36 +1152,41 @@ class RequestParameterFilter < BaseRequestTest
end
class RequestEtag < BaseRequestTest
- test "if_none_match_etags none" do
+ test "always matches *" do
+ request = stub_request('HTTP_IF_NONE_MATCH' => '*')
+
+ assert_equal '*', request.if_none_match
+ assert_equal ['*'], request.if_none_match_etags
+
+ assert request.etag_matches?('"strong"')
+ assert request.etag_matches?('W/"weak"')
+ assert_not request.etag_matches?(nil)
+ end
+
+ test "doesn't match absent If-None-Match" do
request = stub_request
assert_equal nil, request.if_none_match
assert_equal [], request.if_none_match_etags
- assert !request.etag_matches?("foo")
- assert !request.etag_matches?(nil)
- end
- test "if_none_match_etags single" do
- header = 'the-etag'
- request = stub_request('HTTP_IF_NONE_MATCH' => header)
-
- assert_equal header, request.if_none_match
- assert_equal [header], request.if_none_match_etags
- assert request.etag_matches?("the-etag")
+ assert_not request.etag_matches?("foo")
+ assert_not request.etag_matches?(nil)
end
- test "if_none_match_etags quoted single" do
+ test "matches opaque ETag validators without unquoting" do
header = '"the-etag"'
request = stub_request('HTTP_IF_NONE_MATCH' => header)
assert_equal header, request.if_none_match
- assert_equal ['the-etag'], request.if_none_match_etags
- assert request.etag_matches?("the-etag")
+ assert_equal ['"the-etag"'], request.if_none_match_etags
+
+ assert request.etag_matches?('"the-etag"')
+ assert_not request.etag_matches?("the-etag")
end
test "if_none_match_etags multiple" do
header = 'etag1, etag2, "third etag", "etag4"'
- expected = ['etag1', 'etag2', 'third etag', 'etag4']
+ expected = ['etag1', 'etag2', '"third etag"', '"etag4"']
request = stub_request('HTTP_IF_NONE_MATCH' => header)
assert_equal header, request.if_none_match
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 8b3849cb7a..658e0d004b 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -189,7 +189,7 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal({"user_name" => "david", "login" => nil}, @response.cookies)
end
- test "read cache control" do
+ test "read ETag and Cache-Control" do
resp = ActionDispatch::Response.new.tap { |response|
response.cache_control[:public] = true
response.etag = '123'
@@ -197,6 +197,9 @@ class ResponseTest < ActiveSupport::TestCase
}
resp.to_a
+ assert resp.etag?
+ assert resp.weak_etag?
+ assert_not resp.strong_etag?
assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.etag)
assert_equal({:public => true}, resp.cache_control)
@@ -204,6 +207,20 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal('W/"202cb962ac59075b964b07152d234b70"', resp.headers['ETag'])
end
+ test "read strong ETag" do
+ resp = ActionDispatch::Response.new.tap { |response|
+ response.cache_control[:public] = true
+ response.strong_etag = '123'
+ response.body = 'Hello'
+ }
+ resp.to_a
+
+ assert resp.etag?
+ assert_not resp.weak_etag?
+ assert resp.strong_etag?
+ assert_equal('"202cb962ac59075b964b07152d234b70"', resp.etag)
+ end
+
test "read charset and content type" do
resp = ActionDispatch::Response.new.tap { |response|
response.charset = 'utf-16'
@@ -445,4 +462,20 @@ class ResponseIntegrationTest < ActionDispatch::IntegrationTest
assert_equal('application/xml; charset=utf-16', @response.headers['Content-Type'])
end
+
+ test "strong ETag validator" do
+ @app = lambda { |env|
+ ActionDispatch::Response.new.tap { |resp|
+ resp.strong_etag = '123'
+ resp.body = 'Hello'
+ resp.request = ActionDispatch::Request.empty
+ }.to_a
+ }
+
+ get '/'
+ assert_response :ok
+
+ assert_equal('"202cb962ac59075b964b07152d234b70"', @response.headers['ETag'])
+ assert_equal('"202cb962ac59075b964b07152d234b70"', @response.etag)
+ end
end
diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb
index f72a87b994..9d0d23d6de 100644
--- a/actionpack/test/dispatch/routing/inspector_test.rb
+++ b/actionpack/test/dispatch/routing/inspector_test.rb
@@ -133,7 +133,9 @@ module ActionDispatch
def test_inspect_routes_shows_dynamic_action_route
output = draw do
- get 'api/:action' => 'api'
+ ActiveSupport::Deprecation.silence do
+ get 'api/:action' => 'api'
+ end
end
assert_equal [
@@ -144,7 +146,9 @@ module ActionDispatch
def test_inspect_routes_shows_controller_and_action_only_route
output = draw do
- get ':controller/:action'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action'
+ end
end
assert_equal [
@@ -155,7 +159,9 @@ module ActionDispatch
def test_inspect_routes_shows_controller_and_action_route_with_constraints
output = draw do
- get ':controller(/:action(/:id))', :id => /\d+/
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action(/:id))', :id => /\d+/
+ end
end
assert_equal [
@@ -164,7 +170,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_route_with_defaults
+ def test_rails_routes_shows_route_with_defaults
output = draw do
get 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'}
end
@@ -175,7 +181,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_route_with_constraints
+ def test_rails_routes_shows_route_with_constraints
output = draw do
get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/
end
@@ -186,7 +192,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_routes_with_dashes
+ def test_rails_routes_shows_routes_with_dashes
output = draw do
get 'about-us' => 'pages#about_us'
get 'our-work/latest'
@@ -209,7 +215,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_route_with_rack_app
+ def test_rails_routes_shows_route_with_rack_app
output = draw do
get 'foo/:id' => MountedRackApp, :id => /[A-Z]\d{5}/
end
@@ -220,7 +226,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_named_route_with_mounted_rack_app
+ def test_rails_routes_shows_named_route_with_mounted_rack_app
output = draw do
mount MountedRackApp => '/foo'
end
@@ -231,7 +237,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_overridden_named_route_with_mounted_rack_app_with_name
+ def test_rails_routes_shows_overridden_named_route_with_mounted_rack_app_with_name
output = draw do
mount MountedRackApp => '/foo', as: 'blog'
end
@@ -242,7 +248,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_shows_route_with_rack_app_nested_with_dynamic_constraints
+ def test_rails_routes_shows_route_with_rack_app_nested_with_dynamic_constraints
constraint = Class.new do
def inspect
"( my custom constraint )"
@@ -261,7 +267,7 @@ module ActionDispatch
], output
end
- def test_rake_routes_dont_show_app_mounted_in_assets_prefix
+ def test_rails_routes_dont_show_app_mounted_in_assets_prefix
output = draw do
get '/sprockets' => MountedRackApp
end
@@ -269,7 +275,7 @@ module ActionDispatch
assert_no_match(/\/sprockets/, output.first)
end
- def test_rake_routes_shows_route_defined_in_under_assets_prefix
+ def test_rails_routes_shows_route_defined_in_under_assets_prefix
output = draw do
scope '/sprockets' do
get '/foo' => 'foo#bar'
@@ -335,7 +341,9 @@ module ActionDispatch
def test_regression_route_with_controller_regexp
output = draw do
- get ':controller(/:action)', controller: /api\/[^\/]+/, format: false
+ ActiveSupport::Deprecation.silence do
+ get ':controller(/:action)', controller: /api\/[^\/]+/, format: false
+ end
end
assert_equal ["Prefix Verb URI Pattern Controller#Action",
@@ -389,6 +397,29 @@ module ActionDispatch
], output
end
+ def test_displaying_routes_for_internal_engines
+ engine = Class.new(Rails::Engine) do
+ def self.inspect
+ "Blog::Engine"
+ end
+ end
+ engine.routes.draw do
+ get '/cart', to: 'cart#show'
+ post '/cart', to: 'cart#create'
+ patch '/cart', to: 'cart#update'
+ end
+
+ output = draw do
+ get '/custom/assets', to: 'custom_assets#show'
+ mount engine => "/blog", as: "blog", internal: true
+ end
+
+ assert_equal [
+ " Prefix Verb URI Pattern Controller#Action",
+ "custom_assets GET /custom/assets(.:format) custom_assets#show",
+ ], output
+ end
+
end
end
end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 5ead9357ae..09830c0c46 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -116,7 +116,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_raise(ArgumentError) do
draw do
namespace :admin do
- get '/:controller(/:action(/:id(.:format)))'
+ ActiveSupport::Deprecation.silence do
+ get '/:controller(/:action(/:id(.:format)))'
+ end
end
end
end
@@ -125,7 +127,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_namespace_without_controller_segment
draw do
namespace :admin do
- get 'hello/:controllers/:action'
+ ActiveSupport::Deprecation.silence do
+ get 'hello/:controllers/:action'
+ end
end
end
get '/admin/hello/foo/new'
@@ -427,7 +431,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get 'global/hide_notice'
get 'global/export', :action => :export, :as => :export_request
get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ }
- get 'global/:action'
+
+ ActiveSupport::Deprecation.silence do
+ get 'global/:action'
+ end
end
end
@@ -450,7 +457,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_local
draw do
- get "/local/:action", :controller => "local"
+ ActiveSupport::Deprecation.silence do
+ get "/local/:action", :controller => "local"
+ end
end
get '/local/dashboard'
@@ -1506,7 +1515,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_not_matching_shorthand_with_dynamic_parameters
draw do
- get ':controller/:action/admin'
+ ActiveSupport::Deprecation.silence do
+ get ':controller/:action/admin'
+ end
end
get '/finances/overview/admin'
@@ -1542,7 +1553,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_scoped_controller_with_namespace_and_action
draw do
namespace :account do
- get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback
+ ActiveSupport::Deprecation.silence do
+ get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback
+ end
end
end
@@ -1837,7 +1850,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_url_generator_for_generic_route
draw do
- get "whatever/:controller(/:action(/:id))"
+ ActiveSupport::Deprecation.silence do
+ get "whatever/:controller(/:action(/:id))"
+ end
end
get '/whatever/foo/bar'
@@ -1849,7 +1864,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_url_generator_for_namespaced_generic_route
draw do
- get "whatever/:controller(/:action(/:id))", :id => /\d+/
+ ActiveSupport::Deprecation.silence do
+ get "whatever/:controller(/:action(/:id))", :id => /\d+/
+ end
end
get '/whatever/foo/bar/show'
@@ -3125,12 +3142,6 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
assert_raise(ArgumentError) do
- assert_deprecated do
- draw { controller("/feeds") { get '/feeds/:service', :to => :show } }
- end
- end
-
- assert_raise(ArgumentError) do
draw { resources :feeds, :controller => '/feeds' }
end
end
@@ -3599,6 +3610,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal '/?id=1', root_path(params)
end
+ def test_dynamic_controller_segments_are_deprecated
+ assert_deprecated do
+ draw do
+ get '/:controller', action: 'index'
+ end
+ end
+ end
+
+ def test_dynamic_action_segments_are_deprecated
+ assert_deprecated do
+ draw do
+ get '/pages/:action', controller: 'pages'
+ end
+ end
+ end
+
private
def draw(&block)
@@ -4122,7 +4149,11 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
app.draw do
ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
get '/foo' => ok, as: :foo
- get '/post(/:action(/:id))' => ok, as: :posts
+
+ ActiveSupport::Deprecation.silence do
+ get '/post(/:action(/:id))' => ok, as: :posts
+ end
+
get '/:foo/:foo_type/bars/:id' => ok, as: :bar
get '/projects/:id.:format' => ok, as: :project
get '/pages/:id' => ok, as: :page
@@ -4292,11 +4323,16 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest
test "invalid UTF-8 encoding returns a 400 Bad Request" do
with_routing do |set|
- set.draw do
- get "/bar/:id", :to => redirect("/foo/show/%{id}")
- get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show"
- get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo"
- get "/:controller(/:action(/:id))"
+ ActiveSupport::Deprecation.silence do
+ set.draw do
+ get "/bar/:id", :to => redirect("/foo/show/%{id}")
+ get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show"
+
+ ActiveSupport::Deprecation.silence do
+ get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo"
+ get "/:controller(/:action(/:id))"
+ end
+ end
end
get "/%E2%EF%BF%BD%A6"
@@ -4627,7 +4663,9 @@ class TestErrorsInController < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new
Routes.draw do
- get '/:controller(/:action)'
+ ActiveSupport::Deprecation.silence do
+ get '/:controller(/:action)'
+ end
end
APP = build_app Routes
diff --git a/actionpack/test/dispatch/session/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb
index dbb996973d..769de1a1e0 100644
--- a/actionpack/test/dispatch/session/cache_store_test.rb
+++ b/actionpack/test/dispatch/session/cache_store_test.rb
@@ -164,7 +164,9 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def with_test_route_set
with_routing do |set|
set.draw do
- get ':action', :to => ::CacheStoreTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ':action', :to => ::CacheStoreTest::TestController
+ end
end
@app = self.class.build_app(set) do |middleware|
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index f07e215e3a..09cb1d925f 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -345,7 +345,9 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
def with_test_route_set(options = {})
with_routing do |set|
set.draw do
- get ':action', :to => ::CookieStoreTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ':action', :to => ::CookieStoreTest::TestController
+ end
end
options = { :key => SessionKey }.merge!(options)
diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb
index 3fed9bad4f..18cb227dad 100644
--- a/actionpack/test/dispatch/session/mem_cache_store_test.rb
+++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb
@@ -187,7 +187,9 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
def with_test_route_set
with_routing do |set|
set.draw do
- get ':action', :to => ::MemCacheStoreTest::TestController
+ ActiveSupport::Deprecation.silence do
+ get ':action', :to => ::MemCacheStoreTest::TestController
+ end
end
@app = self.class.build_app(set) do |middleware|
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index c66a0e6a7a..668b2b6cfe 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -7,7 +7,7 @@ class SSLTest < ActionDispatch::IntegrationTest
def build_app(headers: {}, ssl_options: {})
headers = HEADERS.merge(headers)
- ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options
+ ActionDispatch::SSL.new lambda { |env| [200, headers, []] }, ssl_options.reverse_merge(hsts: { subdomains: true })
end
end
@@ -39,6 +39,13 @@ class RedirectSSLTest < SSLTest
assert_equal redirect[:body].join, @response.body
end
+ test 'exclude can avoid redirect' do
+ excluding = { exclude: -> request { request.path =~ /healthcheck/ } }
+
+ assert_not_redirected 'http://example.org/healthcheck', redirect: excluding
+ assert_redirected from: 'http://example.org/', redirect: excluding
+ end
+
test 'https is not redirected' do
assert_not_redirected 'https://example.org'
end
@@ -98,15 +105,16 @@ end
class StrictTransportSecurityTest < SSLTest
EXPECTED = 'max-age=15552000'
+ EXPECTED_WITH_SUBDOMAINS = 'max-age=15552000; includeSubDomains'
- def assert_hsts(expected, url: 'https://example.org', hsts: {}, headers: {})
+ def assert_hsts(expected, url: 'https://example.org', hsts: { subdomains: true }, headers: {})
self.app = build_app ssl_options: { hsts: hsts }, headers: headers
get url
assert_equal expected, response.headers['Strict-Transport-Security']
end
test 'enabled by default' do
- assert_hsts EXPECTED
+ assert_hsts EXPECTED_WITH_SUBDOMAINS
end
test 'not sent with http:// responses' do
@@ -126,11 +134,15 @@ class StrictTransportSecurityTest < SSLTest
end
test ':expires sets max-age' do
- assert_hsts 'max-age=500', hsts: { expires: 500 }
+ assert_deprecated do
+ assert_hsts 'max-age=500', hsts: { expires: 500 }
+ end
end
test ':expires supports AS::Duration arguments' do
- assert_hsts 'max-age=31557600', hsts: { expires: 1.year }
+ assert_deprecated do
+ assert_hsts 'max-age=31557600', hsts: { expires: 1.year }
+ end
end
test 'include subdomains' do
@@ -142,11 +154,15 @@ class StrictTransportSecurityTest < SSLTest
end
test 'opt in to browser preload lists' do
- assert_hsts "#{EXPECTED}; preload", hsts: { preload: true }
+ assert_deprecated do
+ assert_hsts "#{EXPECTED}; preload", hsts: { preload: true }
+ end
end
test 'opt out of browser preload lists' do
- assert_hsts EXPECTED, hsts: { preload: false }
+ assert_deprecated do
+ assert_hsts EXPECTED, hsts: { preload: false }
+ end
end
end
diff --git a/actionpack/test/fixtures/collection_cache/index.html.erb b/actionpack/test/fixtures/collection_cache/index.html.erb
index 521b1450df..853e501ab4 100644
--- a/actionpack/test/fixtures/collection_cache/index.html.erb
+++ b/actionpack/test/fixtures/collection_cache/index.html.erb
@@ -1 +1 @@
-<%= render @customers %> \ No newline at end of file
+<%= render partial: 'customers/customer', collection: @customers, cached: true %>
diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb
index 01453323ef..951c761995 100644
--- a/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb
+++ b/actionpack/test/fixtures/functional_caching/fragment_cached_with_options.html.erb
@@ -1,3 +1,3 @@
<body>
-<%= cache 'with_options', skip_digest: true, expires_in: 1.minute do %><p>ERB</p><% end %>
+<%= cache 'with_options', skip_digest: true, expires_in: 10 do %><p>ERB</p><% end %>
</body>
diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb
new file mode 100644
index 0000000000..ded99ba52d
--- /dev/null
+++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_mobile_variant.html+mobile.erb
@@ -0,0 +1 @@
+mobile
diff --git a/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb
new file mode 100644
index 0000000000..dd294f8cf6
--- /dev/null
+++ b/actionpack/test/fixtures/implicit_render_test/empty_action_with_template.html.erb
@@ -0,0 +1 @@
+<h1>Empty action rendered this implicitly.</h1>
diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb
index 317801ad30..317801ad30 100644
--- a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb
+++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_template_rendering.html+mobile.erb
diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb
index 15d51e5d6c..75caf56d32 100644
--- a/actionpack/test/journey/router_test.rb
+++ b/actionpack/test/journey/router_test.rb
@@ -3,7 +3,7 @@ require 'abstract_unit'
module ActionDispatch
module Journey
class TestRouter < ActiveSupport::TestCase
- attr_reader :routes, :mapper
+ attr_reader :mapper, :routes, :route_set, :router
def setup
@app = Routing::RouteSet::Dispatcher.new({})
@@ -15,36 +15,36 @@ module ActionDispatch
end
def test_dashes
- mapper.get '/foo-bar-baz', to: 'foo#bar'
+ get '/foo-bar-baz', to: 'foo#bar'
env = rails_env 'PATH_INFO' => '/foo-bar-baz'
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
assert called
end
def test_unicode
- mapper.get '/ほげ', to: 'foo#bar'
+ get '/ほげ', to: 'foo#bar'
#match the escaped version of /ほげ
env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92'
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
assert called
end
def test_regexp_first_precedence
- mapper.get "/whois/:domain", :domain => /\w+\.[\w\.]+/, to: "foo#bar"
- mapper.get "/whois/:id(.:format)", to: "foo#baz"
+ get "/whois/:domain", :domain => /\w+\.[\w\.]+/, to: "foo#bar"
+ get "/whois/:id(.:format)", to: "foo#baz"
env = rails_env 'PATH_INFO' => '/whois/example.com'
list = []
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
list << r
end
assert_equal 2, list.length
@@ -55,7 +55,7 @@ module ActionDispatch
end
def test_required_parts_verified_are_anchored
- mapper.get "/foo/:id", :id => /\d/, anchor: false, to: "foo#bar"
+ get "/foo/:id", :id => /\d/, anchor: false, to: "foo#bar"
assert_raises(ActionController::UrlGenerationError) do
@formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
@@ -63,7 +63,7 @@ module ActionDispatch
end
def test_required_parts_are_verified_when_building
- mapper.get "/foo/:id", :id => /\d+/, anchor: false, to: "foo#bar"
+ get "/foo/:id", :id => /\d+/, anchor: false, to: "foo#bar"
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
assert_equal '/foo/10', path
@@ -74,7 +74,7 @@ module ActionDispatch
end
def test_only_required_parts_are_verified
- mapper.get "/foo(/:id)", :id => /\d/, :to => "foo#bar"
+ get "/foo(/:id)", :id => /\d/, :to => "foo#bar"
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :id => '10' }, { })
assert_equal '/foo/10', path
@@ -88,8 +88,7 @@ module ActionDispatch
def test_knows_what_parts_are_missing_from_named_route
route_name = "gorby_thunderhorse"
- mapper = ActionDispatch::Routing::Mapper.new @route_set
- mapper.get "/foo/:id", :as => route_name, :id => /\d+/, :to => "foo#bar"
+ get "/foo/:id", :as => route_name, :id => /\d+/, :to => "foo#bar"
error = assert_raises(ActionController::UrlGenerationError) do
@formatter.generate(route_name, { }, { })
@@ -109,19 +108,16 @@ module ActionDispatch
end
def test_X_Cascade
- mapper.get "/messages(.:format)", to: "foo#bar"
- resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }))
+ get "/messages(.:format)", to: "foo#bar"
+ resp = router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }))
assert_equal ['Not Found'], resp.last
assert_equal 'pass', resp[1]['X-Cascade']
assert_equal 404, resp.first
end
def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes
- route_set = Routing::RouteSet.new
- mapper = Routing::Mapper.new route_set
-
app = lambda { |env| [200, {}, ['success!']] }
- mapper.get '/weblog', :to => app
+ get '/weblog', :to => app
env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog')
resp = route_set.call env
@@ -130,38 +126,38 @@ module ActionDispatch
end
def test_defaults_merge_correctly
- mapper.get '/foo(/:id)', to: "foo#bar", id: nil
+ get '/foo(/:id)', to: "foo#bar", id: nil
env = rails_env 'PATH_INFO' => '/foo/10'
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal({:id => '10', :controller => "foo", :action => "bar"}, params)
end
env = rails_env 'PATH_INFO' => '/foo'
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal({:id => nil, :controller => "foo", :action => "bar"}, params)
end
end
def test_recognize_with_unbound_regexp
- mapper.get "/foo", anchor: false, to: "foo#bar"
+ get "/foo", anchor: false, to: "foo#bar"
env = rails_env 'PATH_INFO' => '/foo/bar'
- @router.recognize(env) { |*_| }
+ router.recognize(env) { |*_| }
assert_equal '/foo', env.env['SCRIPT_NAME']
assert_equal '/bar', env.env['PATH_INFO']
end
def test_bound_regexp_keeps_path_info
- mapper.get "/foo", to: "foo#bar"
+ get "/foo", to: "foo#bar"
env = rails_env 'PATH_INFO' => '/foo'
before = env.env['SCRIPT_NAME']
- @router.recognize(env) { |*_| }
+ router.recognize(env) { |*_| }
assert_equal before, env.env['SCRIPT_NAME']
assert_equal '/foo', env.env['PATH_INFO']
@@ -174,41 +170,41 @@ module ActionDispatch
"/messages/:id/edit(.:format)",
"/messages/:id(.:format)"
].each do |path|
- mapper.get path, to: "foo#bar"
+ get path, to: "foo#bar"
end
env = rails_env 'PATH_INFO' => '/messages/unknown/path'
yielded = false
- @router.recognize(env) do |*whatever|
+ router.recognize(env) do |*whatever|
yielded = true
end
assert_not yielded
end
def test_required_part_in_recall
- mapper.get "/messages/:a/:b", to: "foo#bar"
+ get "/messages/:a/:b", to: "foo#bar"
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar", :a => 'a' }, { :b => 'b' })
assert_equal "/messages/a/b", path
end
def test_splat_in_recall
- mapper.get "/*path", to: "foo#bar"
+ get "/*path", to: "foo#bar"
path, _ = @formatter.generate(nil, { :controller => "foo", :action => "bar" }, { :path => 'b' })
assert_equal "/b", path
end
def test_recall_should_be_used_when_scoring
- mapper.get "/messages/:action(/:id(.:format))", to: 'foo#bar'
- mapper.get "/messages/:id(.:format)", to: 'bar#baz'
+ get "/messages/:action(/:id(.:format))", to: 'foo#bar'
+ get "/messages/:id(.:format)", to: 'bar#baz'
path, _ = @formatter.generate(nil, { :controller => "foo", :id => 10 }, { :action => 'index' })
assert_equal "/messages/index/10", path
end
def test_nil_path_parts_are_ignored
- mapper.get "/:controller(/:action(.:format))", to: "tasks#lol"
+ get "/:controller(/:action(.:format))", to: "tasks#lol"
params = { :controller => "tasks", :format => nil }
extras = { :action => 'lol' }
@@ -220,14 +216,14 @@ module ActionDispatch
def test_generate_slash
params = [ [:controller, "tasks"],
[:action, "show"] ]
- mapper.get "/", Hash[params]
+ get "/", Hash[params]
path, _ = @formatter.generate(nil, Hash[params], {})
assert_equal '/', path
end
def test_generate_calls_param_proc
- mapper.get '/:controller(/:action)', to: "foo#bar"
+ get '/:controller(/:action)', to: "foo#bar"
parameterized = []
params = [ [:controller, "tasks"],
@@ -243,7 +239,7 @@ module ActionDispatch
end
def test_generate_id
- mapper.get '/:controller(/:action)', to: 'foo#bar'
+ get '/:controller(/:action)', to: 'foo#bar'
path, params = @formatter.generate(
nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {})
@@ -252,7 +248,7 @@ module ActionDispatch
end
def test_generate_escapes
- mapper.get '/:controller(/:action)', to: "foo#bar"
+ get '/:controller(/:action)', to: "foo#bar"
path, _ = @formatter.generate(nil,
{ :controller => "tasks",
@@ -262,7 +258,7 @@ module ActionDispatch
end
def test_generate_escapes_with_namespaced_controller
- mapper.get '/:controller(/:action)', to: "foo#bar"
+ get '/:controller(/:action)', to: "foo#bar"
path, _ = @formatter.generate(
nil, { :controller => "admin/tasks",
@@ -272,7 +268,7 @@ module ActionDispatch
end
def test_generate_extra_params
- mapper.get '/:controller(/:action)', to: "foo#bar"
+ get '/:controller(/:action)', to: "foo#bar"
path, params = @formatter.generate(
nil, { :id => 1,
@@ -285,7 +281,7 @@ module ActionDispatch
end
def test_generate_missing_keys_no_matches_different_format_keys
- mapper.get '/:controller/:action/:name', to: "foo#bar"
+ get '/:controller/:action/:name', to: "foo#bar"
primarty_parameters = {
:id => 1,
:controller => "tasks",
@@ -311,7 +307,7 @@ module ActionDispatch
end
def test_generate_uses_recall_if_needed
- mapper.get '/:controller(/:action(/:id))', to: "foo#bar"
+ get '/:controller(/:action(/:id))', to: "foo#bar"
path, params = @formatter.generate(
nil,
@@ -322,7 +318,7 @@ module ActionDispatch
end
def test_generate_with_name
- mapper.get '/:controller(/:action)', to: 'foo#bar', as: 'tasks'
+ get '/:controller(/:action)', to: 'foo#bar', as: 'tasks'
path, params = @formatter.generate(
"tasks",
@@ -338,13 +334,13 @@ module ActionDispatch
'/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" },
}.each do |request_path, expected|
define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do
- mapper.get "/:controller(/:action(/:id))", to: 'foo#bar'
+ get "/:controller(/:action(/:id))", to: 'foo#bar'
route = @routes.first
env = rails_env 'PATH_INFO' => request_path
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal route, r
assert_equal({ :action => "bar" }.merge(expected), params)
called = true
@@ -359,13 +355,13 @@ module ActionDispatch
:splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }]
}.each do |name, (request_path, expected)|
define_method("test_recognize_#{name}") do
- mapper.get '/:segment/*splat', to: 'foo#bar'
+ get '/:segment/*splat', to: 'foo#bar'
env = rails_env 'PATH_INFO' => request_path
called = false
route = @routes.first
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected.merge(:controller=>"foo", :action=>"bar"), params)
called = true
@@ -376,7 +372,7 @@ module ActionDispatch
end
def test_namespaced_controller
- mapper.get "/:controller(/:action(/:id))", { :controller => /.+?/ }
+ get "/:controller(/:action(/:id))", { :controller => /.+?/ }
route = @routes.first
env = rails_env 'PATH_INFO' => '/admin/users/show/10'
@@ -387,7 +383,7 @@ module ActionDispatch
:id => '10'
}
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected, params)
called = true
@@ -396,13 +392,13 @@ module ActionDispatch
end
def test_recognize_literal
- mapper.get "/books(/:action(.:format))", controller: "books"
+ get "/books(/:action(.:format))", controller: "books"
route = @routes.first
env = rails_env 'PATH_INFO' => '/books/list.rss'
expected = { :controller => 'books', :action => 'list', :format => 'rss' }
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
assert_equal route, r
assert_equal(expected, params)
called = true
@@ -412,7 +408,7 @@ module ActionDispatch
end
def test_recognize_head_route
- mapper.match "/books(/:action(.:format))", via: 'head', to: 'foo#bar'
+ match "/books(/:action(.:format))", via: 'head', to: 'foo#bar'
env = rails_env(
'PATH_INFO' => '/books/list.rss',
@@ -420,7 +416,7 @@ module ActionDispatch
)
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
@@ -428,13 +424,13 @@ module ActionDispatch
end
def test_recognize_head_request_as_get_route
- mapper.get "/books(/:action(.:format))", to: 'foo#bar'
+ get "/books(/:action(.:format))", to: 'foo#bar'
env = rails_env 'PATH_INFO' => '/books/list.rss',
"REQUEST_METHOD" => "HEAD"
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
@@ -442,13 +438,13 @@ module ActionDispatch
end
def test_recognize_cares_about_get_verbs
- mapper.match "/books(/:action(.:format))", to: "foo#bar", via: :get
+ match "/books(/:action(.:format))", to: "foo#bar", via: :get
env = rails_env 'PATH_INFO' => '/books/list.rss',
"REQUEST_METHOD" => "POST"
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
@@ -456,13 +452,13 @@ module ActionDispatch
end
def test_recognize_cares_about_post_verbs
- mapper.match "/books(/:action(.:format))", to: "foo#bar", via: :post
+ match "/books(/:action(.:format))", to: "foo#bar", via: :post
env = rails_env 'PATH_INFO' => '/books/list.rss',
"REQUEST_METHOD" => "POST"
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
@@ -470,14 +466,14 @@ module ActionDispatch
end
def test_multi_verb_recognition
- mapper.match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get]
+ match "/books(/:action(.:format))", to: "foo#bar", via: [:post, :get]
%w( POST GET ).each do |verb|
env = rails_env 'PATH_INFO' => '/books/list.rss',
"REQUEST_METHOD" => verb
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
@@ -488,7 +484,7 @@ module ActionDispatch
"REQUEST_METHOD" => 'PUT'
called = false
- @router.recognize(env) do |r, params|
+ router.recognize(env) do |r, params|
called = true
end
@@ -497,6 +493,18 @@ module ActionDispatch
private
+ def get *args
+ ActiveSupport::Deprecation.silence do
+ mapper.get(*args)
+ end
+ end
+
+ def match *args
+ ActiveSupport::Deprecation.silence do
+ mapper.match(*args)
+ end
+ end
+
def rails_env env, klass = ActionDispatch::Request
klass.new(rack_env(env))
end
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 256b90784a..a1901e8a17 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,46 @@
+* Deprecate `datetime_field` and `datetime_field_tag` helpers.
+ Datetime input type was removed from HTML specification.
+ One can use `datetime_local_field` and `datetime_local_field_tag` instead.
+
+ *Wojciech Wnętrzak*
+
+* Added log "Rendering ...", when starting to render a template to log that
+ we have started rendering something. This helps to easily identify the origin
+ of queries in the log whether they came from controller or views.
+
+ *Vipul A M and Prem Sichanugrist*
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* Collection rendering can cache and fetch multiple partials at once.
+
+ Collections rendered as:
+
+ ```ruby
+ <%= render partial: 'notifications/notification', collection: @notifications, as: :notification, cached: true %>
+ ```
+
+ will read several partials from cache at once. The templates in the collection
+ that haven't been cached already will automatically be written to cache. Works
+ great alongside individual template fragment caching. For instance if the
+ template the collection renders is cached like:
+
+ ```ruby
+ # notifications/_notification.html.erb
+ <% cache notification do %>
+ <%# ... %>
+ <% end %>
+ ```
+
+ Then any collection renders shares that cache when attempting to read multiple
+ ones at once.
+
+ *Kasper Timm Hansen*
+
+* Add support for nested hashes/arrays to `:params` option of `button_to` helper.
+
+ *James Coleman*
+
## Rails 5.0.0.beta2 (February 01, 2016) ##
* Fix stripping the digest from the automatically generated img tag alt
@@ -7,7 +50,7 @@
* Create a new `ActiveSupport::SafeBuffer` instance when `content_for` is flushed.
- Fixes #19890
+ Fixes #19890.
*Yoong Kang Lim*
@@ -68,7 +111,7 @@
*Vasiliy Ermolovich*
-* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising a error
+* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising an error
when the only input on the form is the `collection_radio_buttons`.
*Mauro George*
@@ -193,26 +236,6 @@
*Ulisses Almeida*
-* Collection rendering automatically caches and fetches multiple partials.
-
- Collections rendered as:
-
- ```ruby
- <%= render @notifications %>
- <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %>
- ```
-
- will now read several partials from cache at once, if the template starts with a cache call:
-
- ```ruby
- # notifications/_notification.html.erb
- <% cache notification do %>
- <%# ... %>
- <% end %>
- ```
-
- *Kasper Timm Hansen*
-
* Fixed a dependency tracker bug that caused template dependencies not
count layouts as dependencies for partials.
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
index 612e94021d..8b0e031dee 100644
--- a/actionview/actionview.gemspec
+++ b/actionview/actionview.gemspec
@@ -9,11 +9,11 @@ Gem::Specification.new do |s|
s.required_ruby_version = '>= 2.2.2'
- s.license = 'MIT'
+ s.license = 'MIT'
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
s.require_path = 'lib'
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
index 3a6cf63803..b99d1af998 100644
--- a/actionview/lib/action_view/digestor.rb
+++ b/actionview/lib/action_view/digestor.rb
@@ -4,13 +4,11 @@ require 'monitor'
module ActionView
class Digestor
- cattr_reader(:cache)
- @@cache = Concurrent::Map.new
- @@digest_monitor = Monitor.new
+ @@digest_mutex = Mutex.new
class PerRequestDigestCacheExpiry < Struct.new(:app) # :nodoc:
def call(env)
- ActionView::Digestor.cache.clear
+ ActionView::LookupContext::DetailsKey.clear
app.call(env)
end
end
@@ -22,111 +20,104 @@ module ActionView
# * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt>
# * <tt>dependencies</tt> - An array of dependent views
# * <tt>partial</tt> - Specifies whether the template is a partial
- def digest(options)
- options.assert_valid_keys(:name, :finder, :dependencies, :partial)
-
- cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
+ def digest(name:, finder:, dependencies: [])
+ dependencies ||= []
+ cache_key = ([ name ].compact + dependencies).join('.')
# this is a correctly done double-checked locking idiom
# (Concurrent::Map's lookups have volatile semantics)
- @@cache[cache_key] || @@digest_monitor.synchronize do
- @@cache.fetch(cache_key) do # re-check under lock
- compute_and_store_digest(cache_key, options)
+ finder.digest_cache[cache_key] || @@digest_mutex.synchronize do
+ finder.digest_cache.fetch(cache_key) do # re-check under lock
+ partial = name.include?("/_")
+ root = tree(name, finder, partial)
+ dependencies.each do |injected_dep|
+ root.children << Injected.new(injected_dep, nil, nil)
+ end
+ finder.digest_cache[cache_key] = root.digest(finder)
end
end
end
- private
- def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock
- klass = if options[:partial] || options[:name].include?("/_")
- # Prevent re-entry or else recursive templates will blow the stack.
- # There is no need to worry about other threads seeing the +false+ value,
- # as they will then have to wait for this thread to let go of the @@digest_monitor lock.
- pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
- PartialDigestor
- else
- Digestor
- end
-
- @@cache[cache_key] = stored_digest = klass.new(options).digest
- ensure
- # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
- @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
- end
- end
-
- attr_reader :name, :finder, :options
+ def logger
+ ActionView::Base.logger || NullLogger
+ end
- def initialize(options)
- @name, @finder = options.values_at(:name, :finder)
- @options = options.except(:name, :finder)
- end
+ # Create a dependency tree for template named +name+.
+ def tree(name, finder, partial = false, seen = {})
+ logical_name = name.gsub(%r|/_|, "/")
- def digest
- Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
- logger.debug " Cache digest for #{template.inspect}: #{digest}"
- end
- rescue ActionView::MissingTemplate
- logger.error " Couldn't find template for digesting: #{name}"
- ''
- end
+ if finder.disable_cache { finder.exists?(logical_name, [], partial) }
+ template = finder.disable_cache { finder.find(logical_name, [], partial) }
- def dependencies
- DependencyTracker.find_dependencies(name, template, finder.view_paths)
- rescue ActionView::MissingTemplate
- logger.error " '#{name}' file doesn't exist, so no dependencies"
- []
- end
+ if node = seen[template.identifier] # handle cycles in the tree
+ node
+ else
+ node = seen[template.identifier] = Node.create(name, logical_name, template, partial)
- def nested_dependencies
- dependencies.collect do |dependency|
- dependencies = PartialDigestor.new(name: dependency, finder: finder).nested_dependencies
- dependencies.any? ? { dependency => dependencies } : dependency
+ deps = DependencyTracker.find_dependencies(name, template, finder.view_paths)
+ deps.uniq { |n| n.gsub(%r|/_|, "/") }.each do |dep_file|
+ node.children << tree(dep_file, finder, true, seen)
+ end
+ node
+ end
+ else
+ logger.error " '#{name}' file doesn't exist, so no dependencies"
+ logger.error " Couldn't find template for digesting: #{name}"
+ seen[name] ||= Missing.new(name, logical_name, nil)
+ end
end
end
- private
- class NullLogger
- def self.debug(_); end
- def self.error(_); end
- end
+ class Node
+ attr_reader :name, :logical_name, :template, :children
- def logger
- ActionView::Base.logger || NullLogger
+ def self.create(name, logical_name, template, partial)
+ klass = partial ? Partial : Node
+ klass.new(name, logical_name, template, [])
end
- def logical_name
- name.gsub(%r|/_|, "/")
+ def initialize(name, logical_name, template, children = [])
+ @name = name
+ @logical_name = logical_name
+ @template = template
+ @children = children
end
- def partial?
- false
+ def digest(finder, stack = [])
+ Digest::MD5.hexdigest("#{template.source}-#{dependency_digest(finder, stack)}")
end
- def template
- @template ||= finder.disable_cache { finder.find(logical_name, [], partial?) }
+ def dependency_digest(finder, stack)
+ children.map do |node|
+ if stack.include?(node)
+ false
+ else
+ finder.digest_cache[node.name] ||= begin
+ stack.push node
+ node.digest(finder, stack).tap { stack.pop }
+ end
+ end
+ end.join("-")
end
- def source
- template.source
+ def to_dep_map
+ children.any? ? { name => children.map(&:to_dep_map) } : name
end
+ end
- def dependency_digest
- template_digests = dependencies.collect do |template_name|
- Digestor.digest(name: template_name, finder: finder, partial: true)
- end
+ class Partial < Node; end
- (template_digests + injected_dependencies).join("-")
- end
+ class Missing < Node
+ def digest(finder, _ = []) '' end
+ end
- def injected_dependencies
- Array.wrap(options[:dependencies])
- end
- end
+ class Injected < Node
+ def digest(finder, _ = []) name end
+ end
- class PartialDigestor < Digestor # :nodoc:
- def partial?
- true
+ class NullLogger
+ def self.debug(_); end
+ def self.error(_); end
end
end
end
diff --git a/actionview/lib/action_view/flows.rb b/actionview/lib/action_view/flows.rb
index bc61920848..4b912f0b2b 100644
--- a/actionview/lib/action_view/flows.rb
+++ b/actionview/lib/action_view/flows.rb
@@ -37,9 +37,8 @@ module ActionView
end
# Try to get stored content. If the content
- # is not available and we are inside the layout
- # fiber, we set that we are waiting for the given
- # key and yield.
+ # is not available and we're inside the layout fiber,
+ # then it will begin waiting for the given key and yield.
def get(key)
return super if @content.key?(key)
@@ -60,8 +59,8 @@ module ActionView
end
# Appends the contents for the given key. This is called
- # by provides and resumes back to the fiber if it is
- # the key it is waiting for.
+ # by providing and resuming back to the fiber,
+ # if that's the key it's waiting for.
def append!(key, value)
super
@fiber.resume if @waiting_for == key
diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb
index bb5c96cb39..efb565bf59 100644
--- a/actionview/lib/action_view/gem_version.rb
+++ b/actionview/lib/action_view/gem_version.rb
@@ -8,7 +8,7 @@ module ActionView
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb
index dba70e284e..c875f5870f 100644
--- a/actionview/lib/action_view/helpers/atom_feed_helper.rb
+++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb
@@ -132,7 +132,7 @@ module ActionView
end
private
- # Delegate to xml builder, first wrapping the element in a xhtml
+ # Delegate to xml builder, first wrapping the element in an xhtml
# namespaced div element if the method and arguments indicate
# that an xhtml_block? is desired.
def method_missing(method, *arguments, &block)
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
index 401f398721..4c7c4b91c6 100644
--- a/actionview/lib/action_view/helpers/cache_helper.rb
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -126,44 +126,29 @@ module ActionView
#
# Now all you have to do is change that timestamp when the helper method changes.
#
- # === Automatic Collection Caching
+ # === Collection Caching
#
- # When rendering collections such as:
+ # When rendering a collection of objects that each use the same partial, a `cached`
+ # option can be passed.
+ # For collections rendered such:
#
- # <%= render @notifications %>
- # <%= render partial: 'notifications/notification', collection: @notifications %>
+ # <%= render partial: 'notifications/notification', collection: @notifications, cached: true %>
#
- # If the notifications/_notification partial starts with a cache call as:
+ # The `cached: true` will make Action View's rendering read several templates
+ # from cache at once instead of one call per template.
#
- # <% cache notification do %>
- # <%= notification.name %>
- # <% end %>
- #
- # The collection can then automatically use any cached renders for that
- # template by reading them at once instead of one by one.
- #
- # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for
- # more information on what cache calls make a template eligible for this
- # collection caching.
- #
- # The automatic cache multi read can be turned off like so:
+ # Templates in the collection not already cached are written to cache.
#
- # <%= render @notifications, cache: false %>
+ # Works great alongside individual template fragment caching.
+ # For instance if the template the collection renders is cached like:
#
- # === Explicit Collection Caching
- #
- # If the partial template doesn't start with a clean cache call as
- # mentioned above, you can still benefit from collection caching by
- # adding a special comment format anywhere in the template, like:
- #
- # <%# Template Collection: notification %>
- # <% my_helper_that_calls_cache(some_arg, notification) do %>
- # <%= notification.name %>
+ # # notifications/_notification.html.erb
+ # <% cache notification do %>
+ # <%# ... %>
# <% end %>
#
- # The pattern used to match these is <tt>/# Template Collection: (\S+)/</tt>,
- # so it's important that you type it out just so.
- # You can only declare one collection in a partial template file.
+ # Any collection renders will find those cached templates when attempting
+ # to read multiple templates at once.
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
name_options = options.slice(:skip_digest, :virtual_path)
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index c1015ffe89..7ced37572e 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -1118,6 +1118,10 @@ module ActionView
# # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" />
#
def datetime_field(object_name, method, options = {})
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ datetime_field is deprecated and will be removed in Rails 5.1.
+ Use datetime_local_field instead.
+ MESSAGE
Tags::DatetimeField.new(object_name, method, self, options).render
end
@@ -1922,8 +1926,6 @@ module ActionView
@object_name.to_s.humanize
end
- model = model.downcase
-
defaults = []
defaults << :"helpers.submit.#{object_name}.#{key}"
defaults << :"helpers.submit.#{key}"
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
index 55dac74d00..cfff0bef5d 100644
--- a/actionview/lib/action_view/helpers/form_tag_helper.rb
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -691,6 +691,10 @@ module ActionView
# * <tt>:step</tt> - The acceptable value granularity.
# * Otherwise accepts the same options as text_field_tag.
def datetime_field_tag(name, value = nil, options = {})
+ ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
+ datetime_field_tag is deprecated and will be removed in Rails 5.1.
+ Use datetime_local_field_tag instead.
+ MESSAGE
text_field_tag(name, value, options.merge(type: :datetime))
end
@@ -862,13 +866,13 @@ module ActionView
def extra_tags_for_form(html_options)
authenticity_token = html_options.delete("authenticity_token")
- method = html_options.delete("method").to_s
+ method = html_options.delete("method").to_s.downcase
method_tag = case method
- when /^get$/i # must be case-insensitive, but can't use downcase as might be nil
+ when 'get'
html_options["method"] = "get"
''
- when /^post$/i, "", nil
+ when 'post', ''
html_options["method"] = "post"
token_tag(authenticity_token, form_options: {
action: html_options["action"],
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
index 161aa031c6..f0222582c7 100644
--- a/actionview/lib/action_view/helpers/number_helper.rb
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -23,7 +23,7 @@ module ActionView
end
end
- # Formats a +number+ into a US phone number (e.g., (555)
+ # Formats a +number+ into a phone number (US by default e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
#
# ==== Options
@@ -35,6 +35,8 @@ module ActionView
# end of the generated number.
# * <tt>:country_code</tt> - Sets the country code for the phone
# number.
+ # * <tt>:pattern</tt> - Specifies how the number is divided into three
+ # groups with the custom regexp to override the default format.
# * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
# the argument is invalid.
#
@@ -52,6 +54,11 @@ module ActionView
#
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".")
# # => +1.123.555.1234 x 1343
+ #
+ # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
+ # # => "(755) 6123-4567"
+ # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/))
+ # # => "133-1234-5678"
def number_to_phone(number, options = {})
return unless number
options = options.symbolize_keys
diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
index 191a881de0..f9784c3483 100644
--- a/actionview/lib/action_view/helpers/sanitize_helper.rb
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -120,7 +120,7 @@ module ActionView
attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
# Vendors the full, link and white list sanitizers.
- # Provided strictly for compatibility and can be removed in Rails 5.
+ # Provided strictly for compatibility and can be removed in Rails 5.1.
def sanitizer_vendor
Rails::Html::Sanitizer
end
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
index 2562504896..42e7358a1d 100644
--- a/actionview/lib/action_view/helpers/tag_helper.rb
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -154,6 +154,7 @@ module ActionView
options.each_pair do |key, value|
if TAG_PREFIXES.include?(key) && value.is_a?(Hash)
value.each_pair do |k, v|
+ next if v.nil?
output << sep
output << prefix_tag_option(key, k, v, escape)
end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
index 3a4561a083..11c7daf4da 100644
--- a/actionview/lib/action_view/helpers/url_helper.rb
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -302,7 +302,7 @@ module ActionView
params = html_options.delete('params')
method = html_options.delete('method').to_s
- method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe
+ method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.freeze.html_safe
form_method = method == 'get' ? 'get' : 'post'
form_options = html_options.delete('form') || {}
@@ -312,9 +312,10 @@ module ActionView
form_options[:'data-remote'] = true if remote
request_token_tag = if form_method == 'post'
- token_tag(nil, form_options: form_options)
+ request_method = method.empty? ? 'post' : method
+ token_tag(nil, form_options: { action: url, method: request_method })
else
- ''
+ ''.freeze
end
html_options = convert_options_to_data_attributes(options, html_options)
@@ -329,8 +330,8 @@ module ActionView
inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
if params
- params.each do |param_name, value|
- inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param)
+ to_form_params(params).each do |param|
+ inner_tags.safe_concat tag(:input, type: "hidden", name: param[:name], value: param[:value])
end
end
content_tag('form', inner_tags, form_options)
@@ -480,7 +481,7 @@ module ActionView
option = html_options.delete(item).presence || next
"#{item.dasherize}=#{ERB::Util.url_encode(option)}"
}.compact
- extras = extras.empty? ? '' : '?' + extras.join('&')
+ extras = extras.empty? ? ''.freeze : '?' + extras.join('&')
encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@")
html_options["href"] = "mailto:#{encoded_email_address}#{extras}"
@@ -558,29 +559,29 @@ module ActionView
def convert_options_to_data_attributes(options, html_options)
if html_options
html_options = html_options.stringify_keys
- html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options)
+ html_options['data-remote'] = 'true'.freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options)
- method = html_options.delete('method')
+ method = html_options.delete('method'.freeze)
add_method_to_attributes!(html_options, method) if method
html_options
else
- link_to_remote_options?(options) ? {'data-remote' => 'true'} : {}
+ link_to_remote_options?(options) ? {'data-remote' => 'true'.freeze} : {}
end
end
def link_to_remote_options?(options)
if options.is_a?(Hash)
- options.delete('remote') || options.delete(:remote)
+ options.delete('remote'.freeze) || options.delete(:remote)
end
end
def add_method_to_attributes!(html_options, method)
- if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/
- html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip
+ if method && method.to_s.downcase != "get".freeze && html_options["rel".freeze] !~ /nofollow/
+ html_options["rel".freeze] = "#{html_options["rel".freeze]} nofollow".lstrip
end
- html_options["data-method"] = method
+ html_options["data-method".freeze] = method
end
def token_tag(token=nil, form_options: {})
@@ -588,13 +589,49 @@ module ActionView
token ||= form_authenticity_token(form_options: form_options)
tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
else
- ''
+ ''.freeze
end
end
def method_tag(method)
tag('input', type: 'hidden', name: '_method', value: method.to_s)
end
+
+ # Returns an array of hashes each containing :name and :value keys
+ # suitable for use as the names and values of form input fields:
+ #
+ # to_form_params(name: 'David', nationality: 'Danish')
+ # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}]
+ #
+ # to_form_params(country: {name: 'Denmark'})
+ # # => [{name: 'country[name]', value: 'Denmark'}]
+ #
+ # to_form_params(countries: ['Denmark', 'Sweden']})
+ # # => [{name: 'countries[]', value: 'Denmark'}, {name: 'countries[]', value: 'Sweden'}]
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # to_form_params({ name: 'Denmark' }, 'country')
+ # # => [{name: 'country[name]', value: 'Denmark'}]
+ def to_form_params(attribute, namespace = nil) # :nodoc:
+ params = []
+ case attribute
+ when Hash
+ attribute.each do |key, value|
+ prefix = namespace ? "#{namespace}[#{key}]" : key
+ params.push(*to_form_params(value, prefix))
+ end
+ when Array
+ array_prefix = "#{namespace}[]"
+ attribute.each do |value|
+ params.push(*to_form_params(value, array_prefix))
+ end
+ else
+ params << { name: namespace, value: attribute.to_param }
+ end
+
+ params.sort_by { |pair| pair[:name] }
+ end
end
end
end
diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb
index 9047dbdd85..5a29c68214 100644
--- a/actionview/lib/action_view/log_subscriber.rb
+++ b/actionview/lib/action_view/log_subscriber.rb
@@ -20,7 +20,23 @@ module ActionView
end
end
alias :render_partial :render_template
- alias :render_collection :render_template
+
+ def render_collection(event)
+ identifier = event.payload[:identifier] || 'templates'
+
+ info do
+ " Rendered collection of #{from_rails_root(identifier)}" \
+ " #{render_count(event.payload)} (#{event.duration.round(1)}ms)"
+ end
+ end
+
+ def start(name, id, payload)
+ if name == "render_template.action_view"
+ log_rendering_start(payload)
+ end
+
+ super
+ end
def logger
ActionView::Base.logger
@@ -38,6 +54,24 @@ module ActionView
def rails_root
@root ||= "#{Rails.root}/"
end
+
+ def render_count(payload)
+ if payload[:cache_hits]
+ "[#{payload[:cache_hits]} / #{payload[:count]} cache hits]"
+ else
+ "[#{payload[:count]} times]"
+ end
+ end
+
+ private
+
+ def log_rendering_start(payload)
+ info do
+ message = " Rendering #{from_rails_root(payload[:identifier])}"
+ message << " within #{from_rails_root(payload[:layout])}" if payload[:layout]
+ message
+ end
+ end
end
end
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
index 126f289f55..626c4b8f5e 100644
--- a/actionview/lib/action_view/lookup_context.rb
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -55,9 +55,7 @@ module ActionView
class DetailsKey #:nodoc:
alias :eql? :equal?
- alias :object_hash :hash
- attr_reader :hash
@details_keys = Concurrent::Map.new
def self.get(details)
@@ -72,8 +70,14 @@ module ActionView
@details_keys.clear
end
+ def self.digest_caches
+ @details_keys.values.map(&:digest_cache)
+ end
+
+ attr_reader :digest_cache
+
def initialize
- @hash = object_hash
+ @digest_cache = Concurrent::Map.new
end
end
@@ -132,6 +136,11 @@ module ActionView
end
alias :template_exists? :exists?
+ def any?(name, prefixes = [], partial = false)
+ @view_paths.exists?(*args_for_any(name, prefixes, partial))
+ end
+ alias :any_templates? :any?
+
# Adds fallbacks to the view paths. Useful in cases when you are rendering
# a :file.
def with_fallbacks
@@ -168,6 +177,32 @@ module ActionView
[user_details, details_key]
end
+ def args_for_any(name, prefixes, partial) # :nodoc:
+ name, prefixes = normalize_name(name, prefixes)
+ details, details_key = detail_args_for_any
+ [name, prefixes, partial || false, details, details_key]
+ end
+
+ def detail_args_for_any # :nodoc:
+ @detail_args_for_any ||= begin
+ details = {}
+
+ registered_details.each do |k|
+ if k == :variants
+ details[k] = :any
+ else
+ details[k] = Accessors::DEFAULT_PROCS[k].call
+ end
+ end
+
+ if @cache
+ [details, DetailsKey.get(details)]
+ else
+ [details, nil]
+ end
+ end
+ end
+
# Support legacy foo.erb names even though we now ignore .erb
# as well as incorrectly putting part of the path in the template
# name instead of the prefix.
@@ -200,6 +235,10 @@ module ActionView
self.view_paths = view_paths
end
+ def digest_cache
+ details_key.digest_cache
+ end
+
def initialize_details(target, details)
registered_details.each do |k|
target[k] = details[k] || Accessors::DEFAULT_PROCS[k].call
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
index 59d869d92d..df14ae09f4 100644
--- a/actionview/lib/action_view/railtie.rb
+++ b/actionview/lib/action_view/railtie.rb
@@ -59,7 +59,7 @@ module ActionView
rake_tasks do |app|
unless app.config.api_only
- load "action_view/tasks/dependencies.rake"
+ load "action_view/tasks/cache_digests.rake"
end
end
end
diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
index aa77a77acf..1dddf53df0 100644
--- a/actionview/lib/action_view/renderer/abstract_renderer.rb
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -15,7 +15,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?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
+ delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
def initialize(lookup_context)
@lookup_context = lookup_context
@@ -35,8 +35,12 @@ module ActionView
end
end
- def instrument(name, options={})
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
+ def instrument(name, **options)
+ options[:identifier] ||= (@template && @template.identifier) || @path
+
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload|
+ yield payload
+ end
end
def prepend_formats(formats)
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
index bdbf03191a..13b4ec6133 100644
--- a/actionview/lib/action_view/renderer/partial_renderer.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -294,7 +294,7 @@ module ActionView
def render(context, options, block)
setup(context, options, block)
- identifier = (@template = find_partial) ? @template.identifier : @path
+ @template = find_partial
@lookup_context.rendered_format ||= begin
if @template && @template.formats.present?
@@ -305,11 +305,9 @@ module ActionView
end
if @collection
- instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
- render_collection
- end
+ render_collection
else
- instrument(:partial, :identifier => identifier) do
+ instrument(:partial) do
render_partial
end
end
@@ -318,15 +316,17 @@ module ActionView
private
def render_collection
- return nil if @collection.blank?
+ instrument(:collection, 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)
- end
+ if @options.key?(:spacer_template)
+ spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
+ end
- cache_collection_render do
- @template ? collection_with_template : collection_without_template
- end.join(spacer).html_safe
+ cache_collection_render(payload) do
+ @template ? collection_with_template : collection_without_template
+ end.join(spacer).html_safe
+ end
end
def render_partial
@@ -521,7 +521,7 @@ module ActionView
def retrieve_variable(path, as)
variable = as || begin
base = path[-1] == "/".freeze ? "".freeze : File.basename(path)
- raise_invalid_identifier(path) unless base =~ /\A_?(.*)(?:\.\w+)*\z/
+ raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/
$1.to_sym
end
if @collection
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 1147963882..f7deba94ce 100644
--- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
+++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/object/try'
-
module ActionView
module CollectionCaching # :nodoc:
extend ActiveSupport::Concern
@@ -11,42 +9,25 @@ module ActionView
end
private
- def cache_collection_render
- return yield unless cache_collection?
+ def cache_collection_render(instrumentation_payload)
+ return yield unless @options[:cached]
keyed_collection = collection_by_cache_keys
- partial_cache = collection_cache.read_multi(*keyed_collection.keys)
+ cached_partials = collection_cache.read_multi(*keyed_collection.keys)
+ instrumentation_payload[:cache_hits] = cached_partials.size
- @collection = keyed_collection.reject { |key, _| partial_cache.key?(key) }.values
- rendered_partials = @collection.any? ? yield.dup : []
+ @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values
+ rendered_partials = @collection.empty? ? [] : yield
- fetch_or_cache_partial(partial_cache, order_by: keyed_collection.each_key) do
- rendered_partials.shift
+ index = 0
+ fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do
+ rendered_partials[index].tap { index += 1 }
end
end
- def cache_collection?
- @options.fetch(:cache, automatic_cache_eligible?)
- end
-
- def automatic_cache_eligible?
- single_template_render? && !callable_cache_key? &&
- @template.eligible_for_collection_caching?(as: @options[:as])
- end
-
- def single_template_render?
- @template # Template is only set when a collection renders one template.
- end
-
- def callable_cache_key?
- @options[:cache].respond_to?(:call)
- end
-
def collection_by_cache_keys
- seed = callable_cache_key? ? @options[:cache] : ->(i) { i }
-
@collection.each_with_object({}) do |item, hash|
- hash[expanded_cache_key(seed.call(item))] = item
+ hash[expanded_cache_key(item)] = item
end
end
@@ -56,12 +37,10 @@ module ActionView
end
def fetch_or_cache_partial(cached_partials, order_by:)
- cache_options = @options[:cache_options] || @locals[:cache_options] || {}
-
- order_by.map do |key|
- cached_partials.fetch(key) do
+ order_by.map do |cache_key|
+ cached_partials.fetch(cache_key) do
yield.tap do |rendered_partial|
- collection_cache.write(key, rendered_partial, cache_options)
+ collection_cache.write(cache_key, rendered_partial)
end
end
end
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/cache_digests.rake
index f394c319c1..045bdf5691 100644
--- a/actionview/lib/action_view/tasks/dependencies.rake
+++ b/actionview/lib/action_view/tasks/cache_digests.rake
@@ -2,13 +2,13 @@ namespace :cache_digests do
desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
task :nested_dependencies => :environment do
abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
- puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).nested_dependencies
+ puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:to_dep_map)
end
desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
task :dependencies => :environment do
abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
- puts JSON.pretty_generate ActionView::Digestor.new(name: CacheDigests.template_name, finder: CacheDigests.finder).dependencies
+ puts JSON.pretty_generate ActionView::Digestor.tree(CacheDigests.template_name, CacheDigests.finder).children.map(&:name)
end
class CacheDigests
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
index 15fc2b71a3..169ee55fdc 100644
--- a/actionview/lib/action_view/template.rb
+++ b/actionview/lib/action_view/template.rb
@@ -130,7 +130,6 @@ module ActionView
@source = source
@identifier = identifier
@handler = handler
- @cache_name = extract_resource_cache_name
@compiled = false
@original_encoding = nil
@locals = details[:locals] || []
@@ -166,10 +165,6 @@ module ActionView
@type ||= Types[@formats.first] if @formats.first
end
- def eligible_for_collection_caching?(as: nil)
- @cache_name == (as || inferred_cache_name).to_s
- end
-
# Receives a view object and return a template similar to self by using @virtual_path.
#
# This method is useful if you have a template object but it does not contain its source
@@ -355,23 +350,5 @@ module ActionView
ActiveSupport::Notifications.instrument("#{action}.action_view".freeze, payload, &block)
end
end
-
- EXPLICIT_COLLECTION = /# Template Collection: (?<resource_name>\w+)/
-
- def extract_resource_cache_name
- if match = @source.match(EXPLICIT_COLLECTION) || resource_cache_call_match
- match[:resource_name]
- end
- end
-
- def resource_cache_call_match
- if @handler.respond_to?(:resource_cache_call_pattern)
- @source.match(@handler.resource_cache_call_pattern)
- end
- end
-
- def inferred_cache_name
- @inferred_cache_name ||= @virtual_path.split('/'.freeze).last.sub('_'.freeze, ''.freeze)
- end
end
end
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
index ccee785d3e..3f38c3d2b9 100644
--- a/actionview/lib/action_view/template/error.rb
+++ b/actionview/lib/action_view/template/error.rb
@@ -135,13 +135,13 @@ module ActionView
end
def formatted_code_for(source_code, line_counter, indent, output)
- start_value = (output == :html) ? {} : ""
+ start_value = (output == :html) ? {} : []
source_code.inject(start_value) do |result, line|
line_counter += 1
if output == :html
result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line])
else
- result << "%#{indent}s: %s\n" % [line_counter, line]
+ result << "%#{indent}s: %s" % [line_counter, line]
end
end
end
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
index 1f8459c24b..85a100ed4c 100644
--- a/actionview/lib/action_view/template/handlers/erb.rb
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -123,31 +123,6 @@ module ActionView
).src
end
- # Returns Regexp to extract a cached resource's name from a cache call at the
- # first line of a template.
- # The extracted cache name is captured as :resource_name.
- #
- # <% cache notification do %> # => notification
- #
- # The pattern should support templates with a beginning comment:
- #
- # <%# Still extractable even though there's a comment %>
- # <% cache notification do %> # => notification
- #
- # But fail to extract a name if a resource association is cached.
- #
- # <% cache notification.event do %> # => nil
- def resource_cache_call_pattern
- /\A
- (?:<%\#.*%>)* # optional initial comment
- \s* # followed by optional spaces or newlines
- <%\s*cache[\(\s] # followed by an ERB call to cache
- \s* # followed by optional spaces or newlines
- (?<resource_name>\w+) # capture the cache call argument as :resource_name
- [\s\)] # followed by a space or close paren
- /xm
- end
-
private
def valid_encoding(string, encoding)
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
index 8a675cd521..f33acc2103 100644
--- a/actionview/lib/action_view/template/resolver.rb
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -222,7 +222,7 @@ module ActionView
end
def find_template_paths(query)
- Dir[query].reject do |filename|
+ Dir[query].uniq.reject do |filename|
File.directory?(filename) ||
# deals with case-insensitive file systems.
!File.fnmatch(query, filename, File::FNM_EXTGLOB)
@@ -245,8 +245,12 @@ module ActionView
partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
query.gsub!(/:action/, partial)
- details.each do |ext, variants|
- query.gsub!(/:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
+ details.each do |ext, candidates|
+ if ext == :variants && candidates == :any
+ query.gsub!(/:#{ext}/, "*")
+ else
+ query.gsub!(/:#{ext}/, "{#{candidates.compact.uniq.join(',')}}")
+ end
end
File.expand_path(query, @path)
@@ -340,7 +344,11 @@ module ActionView
query = escape_entry(File.join(@path, path))
exts = EXTENSIONS.map do |ext, prefix|
- "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
+ if ext == :variants && details[ext] == :any
+ "{#{prefix}*,}"
+ else
+ "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
+ end
end.join
query + exts
diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb
index c233d06ccb..b32567cd66 100644
--- a/actionview/lib/action_view/template/types.rb
+++ b/actionview/lib/action_view/template/types.rb
@@ -1,4 +1,3 @@
-require 'set'
require 'active_support/core_ext/module/attribute_accessors'
module ActionView
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
index 37722013ce..717d6866c5 100644
--- a/actionview/lib/action_view/view_paths.rb
+++ b/actionview/lib/action_view/view_paths.rb
@@ -1,5 +1,3 @@
-require 'action_view/base'
-
module ActionView
module ViewPaths
extend ActiveSupport::Concern
@@ -10,7 +8,7 @@ module ActionView
self._view_paths.freeze
end
- delegate :template_exists?, :view_paths, :formats, :formats=,
+ delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=,
:locale, :locale=, :to => :lookup_context
module ClassMethods
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
index 79173f730f..3256d8fc4d 100644
--- a/actionview/test/abstract_unit.rb
+++ b/actionview/test/abstract_unit.rb
@@ -1,5 +1,3 @@
-require File.expand_path('../../../load_paths', __FILE__)
-
$:.unshift(File.dirname(__FILE__) + '/lib')
$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
$:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers')
@@ -95,12 +93,14 @@ module ActionDispatch
super
return if DrawOnce.drew
- SharedTestRoutes.draw do
- get ':controller(/:action)'
- end
+ ActiveSupport::Deprecation.silence do
+ SharedTestRoutes.draw do
+ get ':controller(/:action)'
+ end
- ActionDispatch::IntegrationTest.app.routes.draw do
- get ':controller(/:action)'
+ ActionDispatch::IntegrationTest.app.routes.draw do
+ get ':controller(/:action)'
+ end
end
DrawOnce.drew = true
diff --git a/actionview/test/fixtures/digestor/messages/peek.html.erb b/actionview/test/fixtures/digestor/messages/peek.html.erb
new file mode 100644
index 0000000000..84885ab0bc
--- /dev/null
+++ b/actionview/test/fixtures/digestor/messages/peek.html.erb
@@ -0,0 +1,2 @@
+<%# Template Dependency: messages/message %>
+<%= render "comments/comments" %>
diff --git a/actionview/test/fixtures/test/_customer.mobile.erb b/actionview/test/fixtures/test/_customer.mobile.erb
new file mode 100644
index 0000000000..d8220afeda
--- /dev/null
+++ b/actionview/test/fixtures/test/_customer.mobile.erb
@@ -0,0 +1 @@
+Hello: <%= customer.name rescue "Anonymous" %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_🍣.erb b/actionview/test/fixtures/test/_🍣.erb
new file mode 100644
index 0000000000..4bbe59410a
--- /dev/null
+++ b/actionview/test/fixtures/test/_🍣.erb
@@ -0,0 +1 @@
+🍣
diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb
index a122fe17c9..65c68fc34a 100644
--- a/actionview/test/lib/controller/fake_models.rb
+++ b/actionview/test/lib/controller/fake_models.rb
@@ -31,27 +31,6 @@ end
class GoodCustomer < Customer
end
-class TicketType < Struct.new(:name)
- extend ActiveModel::Naming
- include ActiveModel::Conversion
- extend ActiveModel::Translation
-
- def initialize(*args)
- super
- @persisted = false
- end
-
- def persisted=(boolean)
- @persisted = boolean
- end
-
- def persisted?
- @persisted
- end
-
- attr_accessor :name
-end
-
class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost)
extend ActiveModel::Naming
include ActiveModel::Conversion
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
index dde757b5a2..d4c5048bde 100644
--- a/actionview/test/template/digestor_test.rb
+++ b/actionview/test/template/digestor_test.rb
@@ -13,34 +13,11 @@ class FixtureTemplate
end
end
-class FixtureFinder
+class FixtureFinder < ActionView::LookupContext
FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
- attr_reader :details, :view_paths
- attr_accessor :formats
- attr_accessor :variants
-
- def initialize
- @details = {}
- @view_paths = ActionView::PathSet.new(['digestor'])
- @formats = []
- @variants = []
- end
-
- def details_key
- details.hash
- end
-
- def find(name, prefixes = [], partial = false, keys = [], options = {})
- partial_name = partial ? name.gsub(%r|/([^/]+)$|, '/_\1') : name
- format = @formats.first.to_s
- format += "+#{@variants.first}" if @variants.any?
-
- FixtureTemplate.new("digestor/#{partial_name}.#{format}.erb")
- end
-
- def disable_cache(&block)
- yield
+ def initialize(details = {})
+ super(ActionView::PathSet.new(['digestor']), details, [])
end
end
@@ -49,6 +26,7 @@ class TemplateDigestorTest < ActionView::TestCase
@cwd = Dir.pwd
@tmp_dir = Dir.mktmpdir
+ ActionView::LookupContext::DetailsKey.clear
FileUtils.cp_r FixtureFinder::FIXTURES_DIR, @tmp_dir
Dir.chdir @tmp_dir
end
@@ -56,7 +34,6 @@ class TemplateDigestorTest < ActionView::TestCase
def teardown
Dir.chdir @cwd
FileUtils.rm_r @tmp_dir
- ActionView::Digestor.cache.clear
end
def test_top_level_change_reflected
@@ -153,12 +130,27 @@ class TemplateDigestorTest < ActionView::TestCase
end
end
+ def test_getting_of_singly_nested_dependencies
+ singly_nested_dependencies = ["messages/header", "messages/form", "messages/message", "events/event", "comments/comment"]
+ assert_equal singly_nested_dependencies, nested_dependencies('messages/edit')
+ end
+
+ def test_getting_of_doubly_nested_dependencies
+ doubly_nested = [{"comments/comments"=>["comments/comment"]}, "messages/message"]
+ assert_equal doubly_nested, nested_dependencies('messages/peek')
+ end
+
def test_nested_template_directory
assert_digest_difference("messages/show") do
change_template("messages/actions/_move")
end
end
+ def test_nested_template_deps
+ nested_deps = ["messages/header", {"comments/comments"=>["comments/comment"]}, "messages/actions/move", "events/event", "messages/something_missing", "messages/something_missing_1", "messages/message", "messages/form"]
+ assert_equal nested_deps, nested_dependencies("messages/show")
+ end
+
def test_recursion_in_renders
assert digest("level/recursion") # assert recursion is possible
assert_not_nil digest("level/recursion") # assert digest is stored
@@ -206,13 +198,14 @@ class TemplateDigestorTest < ActionView::TestCase
def test_details_are_included_in_cache_key
# Cache the template digest.
+ @finder = FixtureFinder.new({:formats => [:html]})
old_digest = digest("events/_event")
# Change the template; the cached digest remains unchanged.
change_template("events/_event")
# The details are changed, so a new cache key is generated.
- finder.details[:foo] = "bar"
+ @finder = FixtureFinder.new
# The cache is busted.
assert_not_equal old_digest, digest("events/_event")
@@ -313,29 +306,29 @@ class TemplateDigestorTest < ActionView::TestCase
def assert_digest_difference(template_name, options = {})
previous_digest = digest(template_name, options)
- ActionView::Digestor.cache.clear
+ finder.digest_cache.clear
yield
- assert previous_digest != digest(template_name, options), "digest didn't change"
- ActionView::Digestor.cache.clear
+ assert_not_equal previous_digest, digest(template_name, options), "digest didn't change"
+ finder.digest_cache.clear
end
def digest(template_name, options = {})
options = options.dup
- finder.formats = [:html]
finder.variants = options.delete(:variants) || []
-
- ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options))
+ ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || []))
end
def dependencies(template_name)
- ActionView::Digestor.new({ name: template_name, finder: finder }).dependencies
+ tree = ActionView::Digestor.tree(template_name, finder)
+ tree.children.map(&:name)
end
def nested_dependencies(template_name)
- ActionView::Digestor.new({ name: template_name, finder: finder }).nested_dependencies
+ tree = ActionView::Digestor.tree(template_name, finder)
+ tree.children.map(&:to_dep_map)
end
def finder
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index 034b8a4bf6..310d0ce514 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -128,8 +128,6 @@ class FormHelperTest < ActionView::TestCase
@post_delegator.title = 'Hello World'
@car = Car.new("#000FFF")
-
- @ticket_type = TicketType.new
end
Routes = ActionDispatch::Routing::RouteSet.new
@@ -138,8 +136,6 @@ class FormHelperTest < ActionView::TestCase
resources :comments
end
- resources :ticket_types
-
namespace :admin do
resources :posts do
resources :comments
@@ -1143,13 +1139,17 @@ class FormHelperTest < ActionView::TestCase
def test_datetime_field
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />}
- assert_dom_equal(expected, datetime_field("post", "written_on"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
end
def test_datetime_field_with_datetime_value
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- assert_dom_equal(expected, datetime_field("post", "written_on"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
end
def test_datetime_field_with_extra_attrs
@@ -1158,20 +1158,26 @@ class FormHelperTest < ActionView::TestCase
min_value = DateTime.new(2000, 6, 15, 20, 45, 30)
max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
step = 60
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step))
+ end
end
def test_datetime_field_with_value_attr
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />}
value = DateTime.new(2013,6,29,13,37)
- assert_dom_equal(expected, datetime_field("post", "written_on", value: value))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on", value: value))
+ end
end
def test_datetime_field_with_timewithzone_value
previous_time_zone, Time.zone = Time.zone, 'UTC'
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />}
@post.written_on = Time.zone.parse('2004-06-15 15:30:45')
- assert_dom_equal(expected, datetime_field("post", "written_on"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
ensure
Time.zone = previous_time_zone
end
@@ -1179,7 +1185,9 @@ class FormHelperTest < ActionView::TestCase
def test_datetime_field_with_nil_value
expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />}
@post.written_on = nil
- assert_dom_equal(expected, datetime_field("post", "written_on"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
end
def test_datetime_field_with_string_values_for_min_and_max
@@ -1187,7 +1195,9 @@ class FormHelperTest < ActionView::TestCase
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
min_value = "2000-06-15T20:45:30.000+0000"
max_value = "2010-08-15T10:25:00.000+0000"
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ end
end
def test_datetime_field_with_invalid_string_values_for_min_and_max
@@ -1195,7 +1205,9 @@ class FormHelperTest < ActionView::TestCase
@post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
min_value = "foo"
max_value = "bar"
- assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ end
end
def test_datetime_local_field
@@ -1876,20 +1888,6 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
- def test_lowercase_model_name_default_submit_button_value
- form_for(@ticket_type) do |f|
- concat f.submit
- end
-
- expected =
- '<form class="new_ticket_type" id="new_ticket_type" action="/ticket_types" accept-charset="UTF-8" method="post">' +
- hidden_fields +
- '<input type="submit" name="commit" value="Create ticket type" data-disable-with="Create ticket type" />' +
- '</form>'
-
- assert_dom_equal expected, output_buffer
- end
-
def test_form_for_with_symbol_object_name
form_for(@post, as: "other_name", html: { id: "create-post" }) do |f|
concat f.label(:title, class: 'post_title')
@@ -2257,7 +2255,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form('/posts', 'new_post', 'new_post') do
- "<input name='commit' data-disable-with='Create post' type='submit' value='Create post' />"
+ "<input name='commit' data-disable-with='Create Post' type='submit' value='Create Post' />"
end
assert_dom_equal expected, output_buffer
@@ -2272,7 +2270,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
- "<input name='commit' data-disable-with='Confirm post changes' type='submit' value='Confirm post changes' />"
+ "<input name='commit' data-disable-with='Confirm Post changes' type='submit' value='Confirm Post changes' />"
end
assert_dom_equal expected, output_buffer
@@ -2300,7 +2298,7 @@ class FormHelperTest < ActionView::TestCase
end
expected = whole_form('/posts/123', 'edit_another_post', 'edit_another_post', method: 'patch') do
- "<input name='commit' data-disable-with='Update your post' type='submit' value='Update your post' />"
+ "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />"
end
assert_dom_equal expected, output_buffer
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
index 07b3fba754..7b93c8dc29 100644
--- a/actionview/test/template/form_tag_helper_test.rb
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -622,7 +622,9 @@ class FormTagHelperTest < ActionView::TestCase
def test_datetime_field_tag
expected = %{<input id="appointment" name="appointment" type="datetime" />}
- assert_dom_equal(expected, datetime_field_tag("appointment"))
+ assert_deprecated do
+ assert_dom_equal(expected, datetime_field_tag("appointment"))
+ end
end
def test_datetime_local_field_tag
diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb
index 4776c18b0b..7683444bf0 100644
--- a/actionview/test/template/log_subscriber_test.rb
+++ b/actionview/test/template/log_subscriber_test.rb
@@ -35,7 +35,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
@view.render(:file => "test/hello_world")
wait
- assert_equal 1, @logger.logged(:info).size
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/Rendering test\/hello_world\.erb/, @logger.logged(:info).first)
assert_match(/Rendered test\/hello_world\.erb/, @logger.logged(:info).last)
end
end
@@ -45,7 +46,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
@view.render(:text => "TEXT")
wait
- assert_equal 1, @logger.logged(:info).size
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/Rendering text template/, @logger.logged(:info).first)
assert_match(/Rendered text template/, @logger.logged(:info).last)
end
end
@@ -55,7 +57,8 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
@view.render(:inline => "<%= 'TEXT' %>")
wait
- assert_equal 1, @logger.logged(:info).size
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/Rendering inline template/, @logger.logged(:info).first)
assert_match(/Rendered inline template/, @logger.logged(:info).last)
end
end
@@ -86,7 +89,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
wait
assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered test\/_customer.erb/, @logger.logged(:info).last)
+ assert_match(/Rendered collection of test\/_customer.erb \[2 times\]/, @logger.logged(:info).last)
end
end
@@ -96,7 +99,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
wait
assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered customers\/_customer\.html\.erb/, @logger.logged(:info).last)
+ assert_match(/Rendered collection of customers\/_customer\.html\.erb \[2 times\]/, @logger.logged(:info).last)
end
end
@@ -106,7 +109,21 @@ class AVLogSubscriberTest < ActiveSupport::TestCase
wait
assert_equal 1, @logger.logged(:info).size
- assert_match(/Rendered collection/, @logger.logged(:info).last)
+ assert_match(/Rendered collection of templates/, @logger.logged(:info).last)
+ end
+ end
+
+ def test_render_collection_with_cached_set
+ Rails.stub(:root, File.expand_path(FIXTURE_LOAD_PATH)) do
+ def @view.view_cache_dependencies; []; end
+ def @view.fragment_cache_key(*); 'ahoy `controller` dependency'; end
+
+ @view.render(partial: 'customers/customer', collection: [ Customer.new('david'), Customer.new('mary') ], cached: true,
+ locals: { greeting: 'hi' })
+ wait
+
+ assert_equal 1, @logger.logged(:info).size
+ assert_match(/Rendered collection of customers\/_customer\.html\.erb \[0 \/ 2 cache hits\]/, @logger.logged(:info).last)
end
end
end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 333e0cca11..ad93236d32 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -207,6 +207,10 @@ module RenderTestCases
assert_nothing_raised { @view.render(:partial => "test/a-in") }
end
+ def test_render_partial_with_unicode_text
+ assert_nothing_raised { @view.render(:partial => "test/🍣") }
+ end
+
def test_render_partial_with_invalid_option_as
e = assert_raises(ArgumentError) { @view.render(:partial => "test/partial_only", :as => 'a-in') }
assert_equal "The value (a-in) of the option `as` is not a valid Ruby identifier; " +
@@ -226,13 +230,13 @@ module RenderTestCases
assert_match %r!method.*doesnt_exist!, e.message
assert_equal "", e.sub_template_message
assert_equal "1", e.line_number
- assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip
+ assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
def test_render_error_indentation
e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise_indentation") }
- error_lines = e.annoted_source_code.split("\n")
+ error_lines = e.annoted_source_code
assert_match %r!error\shere!, e.message
assert_equal "11", e.line_number
assert_equal " 9: <p>Ninth paragraph</p>", error_lines.second
@@ -252,7 +256,7 @@ module RenderTestCases
assert_match %r!method.*doesnt_exist!, e.message
assert_equal "", e.sub_template_message
assert_equal "1", e.line_number
- assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip
+ assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code[0].strip
assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
end
@@ -270,6 +274,11 @@ module RenderTestCases
assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ])
end
+ def test_render_partial_collection_with_partial_name_containing_dot
+ assert_equal "Hello: davidHello: mary",
+ @view.render(:partial => "test/customer.mobile", :collection => [ Customer.new("david"), Customer.new("mary") ])
+ end
+
def test_render_partial_collection_as_by_string
assert_equal "david david davidmary mary mary",
@view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => 'customer')
@@ -628,56 +637,59 @@ class LazyViewRenderTest < ActiveSupport::TestCase
end
end
-class CachedCollectionViewRenderTest < CachedViewRenderTest
+class CachedCollectionViewRenderTest < ActiveSupport::TestCase
class CachedCustomer < Customer; end
- teardown do
- ActionView::PartialRenderer.collection_cache.clear
- end
+ include RenderTestCases
- test "with custom key" do
- customer = Customer.new("david")
- key = cache_key([customer, 'key'], "test/_customer")
+ # Ensure view path cache is primed
+ setup do
+ view_paths = ActionController::Base.view_paths
+ assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class
- ActionView::PartialRenderer.collection_cache.write(key, 'Hello')
+ ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new
- assert_equal "Hello",
- @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] })
+ setup_view(view_paths)
end
- test "with caching with custom key and rendering with different key" do
- customer = Customer.new("david")
- key = cache_key([customer, 'key'], "test/_customer")
+ teardown do
+ GC.start
+ I18n.reload!
+ end
- ActionView::PartialRenderer.collection_cache.write(key, 'Hello')
+ test "collection caching does not cache by default" do
+ customer = Customer.new("david", 1)
+ key = cache_key(customer, "test/_customer")
- assert_equal "Hello: david",
- @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'another_key'] })
+ ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
+
+ assert_not_equal "Cached",
+ @view.render(partial: "test/customer", collection: [customer])
end
- test "automatic caching with inferred cache name" do
- customer = CachedCustomer.new("david")
- key = cache_key(customer, "test/_cached_customer")
+ test "collection caching with partial that doesn't use fragment caching" do
+ customer = Customer.new("david", 1)
+ key = cache_key(customer, "test/_customer")
ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
assert_equal "Cached",
- @view.render(partial: "test/cached_customer", collection: [customer])
+ @view.render(partial: "test/customer", collection: [customer], cached: true)
end
- test "automatic caching with as name" do
- customer = CachedCustomer.new("david")
- key = cache_key(customer, "test/_cached_customer_as")
+ test "collection caching with cached true" do
+ customer = CachedCustomer.new("david", 1)
+ key = cache_key(customer, "test/_cached_customer")
ActionView::PartialRenderer.collection_cache.write(key, 'Cached')
assert_equal "Cached",
- @view.render(partial: "test/cached_customer_as", collection: [customer], as: :buyer)
+ @view.render(partial: "test/cached_customer", collection: [customer], cached: true)
end
private
- def cache_key(names, virtual_path)
+ def cache_key(*names, virtual_path)
digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: []
- @view.fragment_cache_key([ *Array(names), digest ])
+ @view.fragment_cache_key([ *names, digest ])
end
end
diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb
index 575eb9bd28..1a091bd692 100644
--- a/actionview/test/template/resolver_patterns_test.rb
+++ b/actionview/test/template/resolver_patterns_test.rb
@@ -3,17 +3,17 @@ require 'abstract_unit'
class ResolverPatternsTest < ActiveSupport::TestCase
def setup
path = File.expand_path("../../fixtures/", __FILE__)
- pattern = ":prefix/{:formats/,}:action{.:formats,}{.:handlers,}"
+ pattern = ":prefix/{:formats/,}:action{.:formats,}{+:variants,}{.:handlers,}"
@resolver = ActionView::FileSystemResolver.new(path, pattern)
end
def test_should_return_empty_list_for_unknown_path
- templates = @resolver.find_all("unknown", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ templates = @resolver.find_all("unknown", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]})
assert_equal [], templates, "expected an empty list of templates"
end
def test_should_return_template_for_declared_path
- templates = @resolver.find_all("path", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ templates = @resolver.find_all("path", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]})
assert_equal 1, templates.size, "expected one template"
assert_equal "Hello custom patterns!", templates.first.source
assert_equal "custom_pattern/path", templates.first.virtual_path
@@ -21,11 +21,22 @@ class ResolverPatternsTest < ActiveSupport::TestCase
end
def test_should_return_all_templates_when_ambiguous_pattern
- templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ templates = @resolver.find_all("another", "custom_pattern", false, {locale: [], formats: [:html], variants: [], handlers: [:erb]})
assert_equal 2, templates.size, "expected two templates"
assert_equal "Another template!", templates[0].source
assert_equal "custom_pattern/another", templates[0].virtual_path
assert_equal "Hello custom patterns!", templates[1].source
assert_equal "custom_pattern/another", templates[1].virtual_path
end
+
+ def test_should_return_all_variants_for_any
+ templates = @resolver.find_all("hello_world", "test", false, {locale: [], formats: [:html, :text], variants: :any, handlers: [:erb]})
+ assert_equal 3, templates.size, "expected three templates"
+ assert_equal "Hello phone!", templates[0].source
+ assert_equal "test/hello_world", templates[0].virtual_path
+ assert_equal "Hello texty phone!", templates[1].source
+ assert_equal "test/hello_world", templates[1].virtual_path
+ assert_equal "Hello world!", templates[2].source
+ assert_equal "test/hello_world", templates[2].virtual_path
+ end
end
diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb
index 6f7a78ccef..f3956a31f6 100644
--- a/actionview/test/template/tag_helper_test.rb
+++ b/actionview/test/template/tag_helper_test.rb
@@ -173,4 +173,10 @@ class TagHelperTest < ActionView::TestCase
tag('a', { aria => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } })
}
end
+
+ def test_link_to_data_nil_equal
+ div_type1 = content_tag(:div, 'test', { 'data-tooltip' => nil })
+ div_type2 = content_tag(:div, 'test', { data: {tooltip: nil} })
+ assert_dom_equal div_type1, div_type2
+ end
end
diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb
index 921011b073..533c1c3219 100644
--- a/actionview/test/template/template_test.rb
+++ b/actionview/test/template/template_test.rb
@@ -192,38 +192,6 @@ class TestERBTemplate < ActiveSupport::TestCase
assert_match(/\xFC/, e.message)
end
- def test_not_eligible_for_collection_caching_without_cache_call
- [
- "<%= 'Hello' %>",
- "<% cache_customer = 42 %>",
- "<% cache customer.name do %><% end %>",
- "<% my_cache customer do %><% end %>"
- ].each do |body|
- template = new_template(body, virtual_path: "test/foo/_customer")
- assert_not template.eligible_for_collection_caching?, "Template #{body.inspect} should not be eligible for collection caching"
- end
- end
-
- def test_eligible_for_collection_caching_with_cache_call_or_explicit
- [
- "<% cache customer do %><% end %>",
- "<% cache(customer) do %><% end %>",
- "<% cache( customer) do %><% end %>",
- "<% cache( customer ) do %><% end %>",
- "<%cache customer do %><% end %>",
- "<% cache customer do %><% end %>",
- " <% cache customer do %>\n<% end %>\n",
- "<%# comment %><% cache customer do %><% end %>",
- "<%# comment %>\n<% cache customer do %><% end %>",
- "<%# comment\n line 2\n line 3 %>\n<% cache customer do %><% end %>",
- "<%# comment 1 %>\n<%# comment 2 %>\n<% cache customer do %><% end %>",
- "<%# comment 1 %>\n<%# Template Collection: customer %>\n<% my_cache customer do %><% end %>"
- ].each do |body|
- template = new_template(body, virtual_path: "test/foo/_customer")
- assert template.eligible_for_collection_caching?, "Template #{body.inspect} should be eligible for collection caching"
- end
- end
-
def with_external_encoding(encoding)
old = Encoding.default_external
Encoding::Converter.new old, encoding if old != encoding
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
index 3010656166..ab56d80de3 100644
--- a/actionview/test/template/url_helper_test.rb
+++ b/actionview/test/template/url_helper_test.rb
@@ -71,6 +71,34 @@ class UrlHelperTest < ActiveSupport::TestCase
assert_equal 'javascript:history.back()', url_for(:back)
end
+ def test_to_form_params_with_hash
+ assert_equal(
+ [{ name: :name, value: 'David' }, { name: :nationality, value: 'Danish' }],
+ to_form_params(name: 'David', nationality: 'Danish')
+ )
+ end
+
+ def test_to_form_params_with_nested_hash
+ assert_equal(
+ [{ name: 'country[name]', value: 'Denmark' }],
+ to_form_params(country: { name: 'Denmark' })
+ )
+ end
+
+ def test_to_form_params_with_array_nested_in_hash
+ assert_equal(
+ [{ name: 'countries[]', value: 'Denmark' }, { name: 'countries[]', value: 'Sweden' }],
+ to_form_params(countries: ['Denmark', 'Sweden'])
+ )
+ end
+
+ def test_to_form_params_with_namespace
+ assert_equal(
+ [{ name: 'country[name]', value: 'Denmark' }],
+ to_form_params({name: 'Denmark'}, 'country')
+ )
+ end
+
def test_button_to_with_straight_url
assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com")
end
@@ -189,8 +217,22 @@ class UrlHelperTest < ActiveSupport::TestCase
def test_button_to_with_params
assert_dom_equal(
- %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></form>},
- button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"})
+ %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="baz" value="quux" /><input type="hidden" name="foo" value="bar" /></form>},
+ button_to("Hello", "http://www.example.com", params: { foo: :bar, baz: "quux" })
+ )
+ end
+
+ def test_button_to_with_nested_hash_params
+ assert_dom_equal(
+ %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[bar]" value="baz" /></form>},
+ button_to("Hello", "http://www.example.com", params: { foo: { bar: 'baz' } })
+ )
+ end
+
+ def test_button_to_with_nested_array_params
+ assert_dom_equal(
+ %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo[]" value="bar" /></form>},
+ button_to("Hello", "http://www.example.com", params: { foo: ['bar'] })
)
end
@@ -613,7 +655,9 @@ class UrlHelperControllerTest < ActionController::TestCase
to: 'url_helper_controller_test/url_helper#show_named_route',
as: :show_named_route
- get "/:controller(/:action(/:id))"
+ ActiveSupport::Deprecation.silence do
+ get "/:controller(/:action(/:id))"
+ end
get 'url_helper_controller_test/url_helper/normalize_recall_params',
to: UrlHelperController.action(:normalize_recall),
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index 229ef03879..efe46ce5ab 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,3 +1,17 @@
+* Enable class reloading prior to job dispatch, and ensure Active Record
+ connections are returned to the pool when jobs are run in separate threads.
+
+ *Matthew Draper*
+
+* Tune the async adapter for low-footprint dev/test usage. Use a single
+ thread pool for all queues and limit to 0 to #CPU total threads, down from
+ 2 to 10*#CPU per queue.
+
+ *Jeremy Daer*
+
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
* Change the default adapter from inline to async. It's a better default as tests will then not mistakenly
come to rely on behavior happening synchronously. This is especially important with things like jobs kicked off
in Active Record lifecycle callbacks.
diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec
index bc1671b508..e97bb40abf 100644
--- a/activejob/activejob.gemspec
+++ b/activejob/activejob.gemspec
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.homepage = 'http://rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.md', 'lib/**/*']
s.require_path = 'lib'
diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb
index f7e05f7cd3..4b9065cf50 100644
--- a/activejob/lib/active_job.rb
+++ b/activejob/lib/active_job.rb
@@ -32,7 +32,6 @@ module ActiveJob
autoload :Base
autoload :QueueAdapters
autoload :ConfiguredJob
- autoload :AsyncJob
autoload :TestCase
autoload :TestHelper
end
diff --git a/activejob/lib/active_job/async_job.rb b/activejob/lib/active_job/async_job.rb
deleted file mode 100644
index ed7a6e8d9b..0000000000
--- a/activejob/lib/active_job/async_job.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'concurrent/map'
-require 'concurrent/scheduled_task'
-require 'concurrent/executor/thread_pool_executor'
-require 'concurrent/utility/processor_counter'
-
-module ActiveJob
- # == Active Job Async Job
- #
- # When enqueueing jobs with Async Job each job will be executed asynchronously
- # on a +concurrent-ruby+ thread pool. All job data is retained in memory.
- # Because job data is not saved to a persistent datastore there is no
- # additional infrastructure needed and jobs process quickly. The lack of
- # persistence, however, means that all unprocessed jobs will be lost on
- # application restart. Therefore in-memory queue adapters are unsuitable for
- # most production environments but are excellent for development and testing.
- #
- # Read more about Concurrent Ruby {here}[https://github.com/ruby-concurrency/concurrent-ruby].
- #
- # To use Async Job set the queue_adapter config to +:async+.
- #
- # Rails.application.config.active_job.queue_adapter = :async
- #
- # Async Job supports job queues specified with +queue_as+. Queues are created
- # automatically as needed and each has its own thread pool.
- class AsyncJob
-
- DEFAULT_EXECUTOR_OPTIONS = {
- min_threads: [2, Concurrent.processor_count].max,
- max_threads: Concurrent.processor_count * 10,
- auto_terminate: true,
- idletime: 60, # 1 minute
- max_queue: 0, # unlimited
- fallback_policy: :caller_runs # shouldn't matter -- 0 max queue
- }.freeze
-
- QUEUES = Concurrent::Map.new do |hash, queue_name| #:nodoc:
- hash.compute_if_absent(queue_name) { ActiveJob::AsyncJob.create_thread_pool }
- end
-
- class << self
- # Forces jobs to process immediately when testing the Active Job gem.
- # This should only be called from within unit tests.
- def perform_immediately! #:nodoc:
- @perform_immediately = true
- end
-
- # Allows jobs to run asynchronously when testing the Active Job gem.
- # This should only be called from within unit tests.
- def perform_asynchronously! #:nodoc:
- @perform_immediately = false
- end
-
- def create_thread_pool #:nodoc:
- if @perform_immediately
- Concurrent::ImmediateExecutor.new
- else
- Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS)
- end
- end
-
- def enqueue(job_data, queue: 'default') #:nodoc:
- QUEUES[queue].post(job_data) { |job| ActiveJob::Base.execute(job) }
- end
-
- def enqueue_at(job_data, timestamp, queue: 'default') #:nodoc:
- delay = timestamp - Time.current.to_f
- if delay > 0
- Concurrent::ScheduledTask.execute(delay, args: [job_data], executor: QUEUES[queue]) do |job|
- ActiveJob::Base.execute(job)
- end
- else
- enqueue(job_data, queue: queue)
- end
- end
- end
- end
-end
diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb
index 2b6149e84e..a6591c6a05 100644
--- a/activejob/lib/active_job/callbacks.rb
+++ b/activejob/lib/active_job/callbacks.rb
@@ -17,6 +17,11 @@ module ActiveJob
extend ActiveSupport::Concern
include ActiveSupport::Callbacks
+ class << self
+ include ActiveSupport::Callbacks
+ define_callbacks :execute
+ end
+
included do
define_callbacks :perform
define_callbacks :enqueue
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index 19b900a285..f7f882c998 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -79,7 +79,7 @@ module ActiveJob
'queue_name' => queue_name,
'priority' => priority,
'arguments' => serialize_arguments(arguments),
- 'locale' => I18n.locale
+ 'locale' => I18n.locale.to_s
}
end
@@ -108,7 +108,7 @@ module ActiveJob
self.queue_name = job_data['queue_name']
self.priority = job_data['priority']
self.serialized_arguments = job_data['arguments']
- self.locale = job_data['locale'] || I18n.locale
+ self.locale = job_data['locale'] || I18n.locale.to_s
end
private
diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb
index 79d232da4a..7c4151fc90 100644
--- a/activejob/lib/active_job/execution.rb
+++ b/activejob/lib/active_job/execution.rb
@@ -17,8 +17,10 @@ module ActiveJob
end
def execute(job_data) #:nodoc:
- job = deserialize(job_data)
- job.perform_now
+ ActiveJob::Callbacks.run_callbacks(:execute) do
+ job = deserialize(job_data)
+ job.perform_now
+ end
end
end
diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb
index bc88221027..be4fabf545 100644
--- a/activejob/lib/active_job/gem_version.rb
+++ b/activejob/lib/active_job/gem_version.rb
@@ -8,7 +8,7 @@ module ActiveJob
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activejob/lib/active_job/queue_adapters/async_adapter.rb b/activejob/lib/active_job/queue_adapters/async_adapter.rb
index 3fc27f56e7..922bc4afce 100644
--- a/activejob/lib/active_job/queue_adapters/async_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/async_adapter.rb
@@ -1,22 +1,113 @@
-require 'active_job/async_job'
+require 'securerandom'
+require 'concurrent/scheduled_task'
+require 'concurrent/executor/thread_pool_executor'
+require 'concurrent/utility/processor_counter'
module ActiveJob
module QueueAdapters
# == Active Job Async adapter
#
- # When enqueueing jobs with the Async adapter the job will be executed
- # asynchronously using {AsyncJob}[http://api.rubyonrails.org/classes/ActiveJob/AsyncJob.html].
+ # The Async adapter runs jobs with an in-process thread pool.
#
- # To use +AsyncJob+ set the queue_adapter config to +:async+.
+ # This is the default queue adapter. It's well-suited for dev/test since
+ # it doesn't need an external infrastructure, but it's a poor fit for
+ # production since it drops pending jobs on restart.
#
- # Rails.application.config.active_job.queue_adapter = :async
+ # To use this adapter, set queue adapter to +:async+:
+ #
+ # config.active_job.queue_adapter = :async
+ #
+ # To configure the adapter's thread pool, instantiate the adapter and
+ # pass your own config:
+ #
+ # config.active_job.queue_adapter = ActiveJob::QueueAdapters::AsyncAdapter.new \
+ # min_threads: 1,
+ # max_threads: 2 * Concurrent.processor_count,
+ # idletime: 600.seconds
+ #
+ # The adapter uses a {Concurrent Ruby}[https://github.com/ruby-concurrency/concurrent-ruby] thread pool to schedule and execute
+ # jobs. Since jobs share a single thread pool, long-running jobs will block
+ # short-lived jobs. Fine for dev/test; bad for production.
class AsyncAdapter
+ # See {Concurrent::ThreadPoolExecutor}[http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ThreadPoolExecutor.html] for executor options.
+ def initialize(**executor_options)
+ @scheduler = Scheduler.new(**executor_options)
+ end
+
def enqueue(job) #:nodoc:
- ActiveJob::AsyncJob.enqueue(job.serialize, queue: job.queue_name)
+ @scheduler.enqueue JobWrapper.new(job), queue_name: job.queue_name
end
def enqueue_at(job, timestamp) #:nodoc:
- ActiveJob::AsyncJob.enqueue_at(job.serialize, timestamp, queue: job.queue_name)
+ @scheduler.enqueue_at JobWrapper.new(job), timestamp, queue_name: job.queue_name
+ end
+
+ # Gracefully stop processing jobs. Finishes in-progress work and handles
+ # any new jobs following the executor's fallback policy (`caller_runs`).
+ # Waits for termination by default. Pass `wait: false` to continue.
+ def shutdown(wait: true) #:nodoc:
+ @scheduler.shutdown wait: wait
+ end
+
+ # Used for our test suite.
+ def immediate=(immediate) #:nodoc:
+ @scheduler.immediate = immediate
+ end
+
+ # Note that we don't actually need to serialize the jobs since we're
+ # performing them in-process, but we do so anyway for parity with other
+ # adapters and deployment environments. Otherwise, serialization bugs
+ # may creep in undetected.
+ class JobWrapper #:nodoc:
+ def initialize(job)
+ job.provider_job_id = SecureRandom.uuid
+ @job_data = job.serialize
+ end
+
+ def perform
+ Base.execute @job_data
+ end
+ end
+
+ class Scheduler #:nodoc:
+ DEFAULT_EXECUTOR_OPTIONS = {
+ min_threads: 0,
+ max_threads: Concurrent.processor_count,
+ auto_terminate: true,
+ idletime: 60, # 1 minute
+ max_queue: 0, # unlimited
+ fallback_policy: :caller_runs # shouldn't matter -- 0 max queue
+ }.freeze
+
+ attr_accessor :immediate
+
+ def initialize(**options)
+ self.immediate = false
+ @immediate_executor = Concurrent::ImmediateExecutor.new
+ @async_executor = Concurrent::ThreadPoolExecutor.new(DEFAULT_EXECUTOR_OPTIONS.merge(options))
+ end
+
+ def enqueue(job, queue_name:)
+ executor.post(job, &:perform)
+ end
+
+ def enqueue_at(job, timestamp, queue_name:)
+ delay = timestamp - Time.current.to_f
+ if delay > 0
+ Concurrent::ScheduledTask.execute(delay, args: [job], executor: executor, &:perform)
+ else
+ enqueue(job, queue_name: queue_name)
+ end
+ end
+
+ def shutdown(wait: true)
+ @async_executor.shutdown
+ @async_executor.wait_for_termination if wait
+ end
+
+ def executor
+ immediate ? @immediate_executor : @async_executor
+ end
end
end
end
diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
index 8ad5f4de07..0496f8449e 100644
--- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
@@ -2,7 +2,7 @@ module ActiveJob
module QueueAdapters
# == Active Job Inline adapter
#
- # When enqueueing jobs with the Inline adapter the job will be executed
+ # When enqueuing jobs with the Inline adapter the job will be executed
# immediately.
#
# To use the Inline set the queue_adapter config to +:inline+.
diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb
index b249ec09dd..a47caa4a7e 100644
--- a/activejob/lib/active_job/railtie.rb
+++ b/activejob/lib/active_job/railtie.rb
@@ -19,5 +19,14 @@ module ActiveJob
end
end
+ initializer "active_job.set_reloader_hook" do |app|
+ ActiveSupport.on_load(:active_job) do
+ ActiveJob::Callbacks.singleton_class.set_callback(:execute, :around, prepend: true) do |_, inner|
+ app.reloader.wrap do
+ inner.call
+ end
+ end
+ end
+ end
end
end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
index ed0c05c1e5..3feb82d432 100644
--- a/activejob/lib/active_job/test_helper.rb
+++ b/activejob/lib/active_job/test_helper.rb
@@ -4,325 +4,308 @@ require 'active_support/core_ext/hash/keys'
module ActiveJob
# Provides helper methods for testing Active Job
module TestHelper
- extend ActiveSupport::Concern
+ delegate :enqueued_jobs, :enqueued_jobs=,
+ :performed_jobs, :performed_jobs=,
+ to: :queue_adapter
- included do
- def before_setup # :nodoc:
- test_adapter = ActiveJob::QueueAdapters::TestAdapter.new
+ def before_setup # :nodoc:
+ test_adapter = ActiveJob::QueueAdapters::TestAdapter.new
- @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass|
- # only override explicitly set adapters, a quirk of `class_attribute`
- klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter)
- end.map do |klass|
- [klass, klass.queue_adapter].tap do
- klass.queue_adapter = test_adapter
- end
+ @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass|
+ # only override explicitly set adapters, a quirk of `class_attribute`
+ klass.singleton_class.public_instance_methods(false).include?(:_queue_adapter)
+ end.map do |klass|
+ [klass, klass.queue_adapter].tap do
+ klass.queue_adapter = test_adapter
end
-
- clear_enqueued_jobs
- clear_performed_jobs
- super
end
- def after_teardown # :nodoc:
- super
- @old_queue_adapters.each do |(klass, adapter)|
- klass.queue_adapter = adapter
- end
- end
+ clear_enqueued_jobs
+ clear_performed_jobs
+ super
+ end
- # Asserts that the number of enqueued jobs matches the given number.
- #
- # def test_jobs
- # assert_enqueued_jobs 0
- # HelloJob.perform_later('david')
- # assert_enqueued_jobs 1
- # HelloJob.perform_later('abdelkader')
- # assert_enqueued_jobs 2
- # end
- #
- # If a block is passed, that block should cause the specified number of
- # jobs to be enqueued.
- #
- # def test_jobs_again
- # assert_enqueued_jobs 1 do
- # HelloJob.perform_later('cristian')
- # end
- #
- # assert_enqueued_jobs 2 do
- # HelloJob.perform_later('aaron')
- # HelloJob.perform_later('rafael')
- # end
- # end
- #
- # The number of times a specific job is enqueued can be asserted.
- #
- # def test_logging_job
- # assert_enqueued_jobs 1, only: LoggingJob do
- # LoggingJob.perform_later
- # HelloJob.perform_later('jeremy')
- # end
- # end
- def assert_enqueued_jobs(number, only: nil)
- if block_given?
- original_count = enqueued_jobs_size(only: only)
- yield
- new_count = enqueued_jobs_size(only: only)
- assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued"
- else
- actual_count = enqueued_jobs_size(only: only)
- assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
- end
+ def after_teardown # :nodoc:
+ super
+ @old_queue_adapters.each do |(klass, adapter)|
+ klass.queue_adapter = adapter
end
+ end
- # Asserts that no jobs have been enqueued.
- #
- # def test_jobs
- # assert_no_enqueued_jobs
- # HelloJob.perform_later('jeremy')
- # assert_enqueued_jobs 1
- # end
- #
- # If a block is passed, that block should not cause any job to be enqueued.
- #
- # def test_jobs_again
- # assert_no_enqueued_jobs do
- # # No job should be enqueued from this block
- # end
- # end
- #
- # It can be asserted that no jobs of a specific kind are enqueued:
- #
- # def test_no_logging
- # assert_no_enqueued_jobs only: LoggingJob do
- # HelloJob.perform_later('jeremy')
- # end
- # end
- #
- # Note: This assertion is simply a shortcut for:
- #
- # assert_enqueued_jobs 0, &block
- def assert_no_enqueued_jobs(only: nil, &block)
- assert_enqueued_jobs 0, only: only, &block
+ # Asserts that the number of enqueued jobs matches the given number.
+ #
+ # def test_jobs
+ # assert_enqueued_jobs 0
+ # HelloJob.perform_later('david')
+ # assert_enqueued_jobs 1
+ # HelloJob.perform_later('abdelkader')
+ # assert_enqueued_jobs 2
+ # end
+ #
+ # If a block is passed, that block should cause the specified number of
+ # jobs to be enqueued.
+ #
+ # def test_jobs_again
+ # assert_enqueued_jobs 1 do
+ # HelloJob.perform_later('cristian')
+ # end
+ #
+ # assert_enqueued_jobs 2 do
+ # HelloJob.perform_later('aaron')
+ # HelloJob.perform_later('rafael')
+ # end
+ # end
+ #
+ # The number of times a specific job is enqueued can be asserted.
+ #
+ # def test_logging_job
+ # assert_enqueued_jobs 1, only: LoggingJob do
+ # LoggingJob.perform_later
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ def assert_enqueued_jobs(number, only: nil)
+ if block_given?
+ original_count = enqueued_jobs_size(only: only)
+ yield
+ new_count = enqueued_jobs_size(only: only)
+ assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued"
+ else
+ actual_count = enqueued_jobs_size(only: only)
+ assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued"
end
+ end
- # Asserts that the number of performed jobs matches the given number.
- # If no block is passed, <tt>perform_enqueued_jobs</tt>
- # must be called around the job call.
- #
- # def test_jobs
- # assert_performed_jobs 0
- #
- # perform_enqueued_jobs do
- # HelloJob.perform_later('xavier')
- # end
- # assert_performed_jobs 1
- #
- # perform_enqueued_jobs do
- # HelloJob.perform_later('yves')
- # assert_performed_jobs 2
- # end
- # end
- #
- # If a block is passed, that block should cause the specified number of
- # jobs to be performed.
- #
- # def test_jobs_again
- # assert_performed_jobs 1 do
- # HelloJob.perform_later('robin')
- # end
- #
- # assert_performed_jobs 2 do
- # HelloJob.perform_later('carlos')
- # HelloJob.perform_later('sean')
- # end
- # end
- #
- # The block form supports filtering. If the :only option is specified,
- # then only the listed job(s) will be performed.
- #
- # def test_hello_job
- # assert_performed_jobs 1, only: HelloJob do
- # HelloJob.perform_later('jeremy')
- # LoggingJob.perform_later
- # end
- # end
- #
- # An array may also be specified, to support testing multiple jobs.
- #
- # def test_hello_and_logging_jobs
- # assert_nothing_raised do
- # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
- # HelloJob.perform_later('jeremy')
- # LoggingJob.perform_later('stewie')
- # RescueJob.perform_later('david')
- # end
- # end
- # end
- def assert_performed_jobs(number, only: nil)
- if block_given?
- original_count = performed_jobs.size
- perform_enqueued_jobs(only: only) { yield }
- new_count = performed_jobs.size
- assert_equal number, new_count - original_count,
- "#{number} jobs expected, but #{new_count - original_count} were performed"
- else
- performed_jobs_size = performed_jobs.size
- assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
- end
- end
+ # Asserts that no jobs have been enqueued.
+ #
+ # def test_jobs
+ # assert_no_enqueued_jobs
+ # HelloJob.perform_later('jeremy')
+ # assert_enqueued_jobs 1
+ # end
+ #
+ # If a block is passed, that block should not cause any job to be enqueued.
+ #
+ # def test_jobs_again
+ # assert_no_enqueued_jobs do
+ # # No job should be enqueued from this block
+ # end
+ # end
+ #
+ # It can be asserted that no jobs of a specific kind are enqueued:
+ #
+ # def test_no_logging
+ # assert_no_enqueued_jobs only: LoggingJob do
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ #
+ # Note: This assertion is simply a shortcut for:
+ #
+ # assert_enqueued_jobs 0, &block
+ def assert_no_enqueued_jobs(only: nil, &block)
+ assert_enqueued_jobs 0, only: only, &block
+ end
- # Asserts that no jobs have been performed.
- #
- # def test_jobs
- # assert_no_performed_jobs
- #
- # perform_enqueued_jobs do
- # HelloJob.perform_later('matthew')
- # assert_performed_jobs 1
- # end
- # end
- #
- # If a block is passed, that block should not cause any job to be performed.
- #
- # def test_jobs_again
- # assert_no_performed_jobs do
- # # No job should be performed from this block
- # end
- # end
- #
- # The block form supports filtering. If the :only option is specified,
- # then only the listed job(s) will be performed.
- #
- # def test_hello_job
- # assert_performed_jobs 1, only: HelloJob do
- # HelloJob.perform_later('jeremy')
- # LoggingJob.perform_later
- # end
- # end
- #
- # An array may also be specified, to support testing multiple jobs.
- #
- # def test_hello_and_logging_jobs
- # assert_nothing_raised do
- # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
- # HelloJob.perform_later('jeremy')
- # LoggingJob.perform_later('stewie')
- # RescueJob.perform_later('david')
- # end
- # end
- # end
- #
- # Note: This assertion is simply a shortcut for:
- #
- # assert_performed_jobs 0, &block
- def assert_no_performed_jobs(only: nil, &block)
- assert_performed_jobs 0, only: only, &block
+ # Asserts that the number of performed jobs matches the given number.
+ # If no block is passed, <tt>perform_enqueued_jobs</tt>
+ # must be called around the job call.
+ #
+ # def test_jobs
+ # assert_performed_jobs 0
+ #
+ # perform_enqueued_jobs do
+ # HelloJob.perform_later('xavier')
+ # end
+ # assert_performed_jobs 1
+ #
+ # perform_enqueued_jobs do
+ # HelloJob.perform_later('yves')
+ # assert_performed_jobs 2
+ # end
+ # end
+ #
+ # If a block is passed, that block should cause the specified number of
+ # jobs to be performed.
+ #
+ # def test_jobs_again
+ # assert_performed_jobs 1 do
+ # HelloJob.perform_later('robin')
+ # end
+ #
+ # assert_performed_jobs 2 do
+ # HelloJob.perform_later('carlos')
+ # HelloJob.perform_later('sean')
+ # end
+ # end
+ #
+ # The block form supports filtering. If the :only option is specified,
+ # then only the listed job(s) will be performed.
+ #
+ # def test_hello_job
+ # assert_performed_jobs 1, only: HelloJob do
+ # HelloJob.perform_later('jeremy')
+ # LoggingJob.perform_later
+ # end
+ # end
+ #
+ # An array may also be specified, to support testing multiple jobs.
+ #
+ # def test_hello_and_logging_jobs
+ # assert_nothing_raised do
+ # assert_performed_jobs 2, only: [HelloJob, LoggingJob] do
+ # HelloJob.perform_later('jeremy')
+ # LoggingJob.perform_later('stewie')
+ # RescueJob.perform_later('david')
+ # end
+ # end
+ # end
+ def assert_performed_jobs(number, only: nil)
+ if block_given?
+ original_count = performed_jobs.size
+ perform_enqueued_jobs(only: only) { yield }
+ new_count = performed_jobs.size
+ assert_equal number, new_count - original_count,
+ "#{number} jobs expected, but #{new_count - original_count} were performed"
+ else
+ performed_jobs_size = performed_jobs.size
+ assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
end
+ end
- # Asserts that the job passed in the block has been enqueued with the given arguments.
- #
- # def test_assert_enqueued_with
- # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do
- # MyJob.perform_later(1,2,3)
- # end
- #
- # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) do
- # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
- # end
- # end
- def assert_enqueued_with(args = {})
- original_enqueued_jobs_count = enqueued_jobs.count
- args.assert_valid_keys(:job, :args, :at, :queue)
- serialized_args = serialize_args_for_assertion(args)
- yield
- in_block_jobs = enqueued_jobs.drop(original_enqueued_jobs_count)
- matching_job = in_block_jobs.find do |job|
- serialized_args.all? { |key, value| value == job[key] }
- end
- assert matching_job, "No enqueued job found with #{args}"
- instantiate_job(matching_job)
- end
+ # Asserts that no jobs have been performed.
+ #
+ # def test_jobs
+ # assert_no_performed_jobs
+ #
+ # perform_enqueued_jobs do
+ # HelloJob.perform_later('matthew')
+ # assert_performed_jobs 1
+ # end
+ # end
+ #
+ # If a block is passed, that block should not cause any job to be performed.
+ #
+ # def test_jobs_again
+ # assert_no_performed_jobs do
+ # # No job should be performed from this block
+ # end
+ # end
+ #
+ # The block form supports filtering. If the :only option is specified,
+ # then only the listed job(s) will not be performed.
+ #
+ # def test_no_logging
+ # assert_no_performed_jobs only: LoggingJob do
+ # HelloJob.perform_later('jeremy')
+ # end
+ # end
+ #
+ # Note: This assertion is simply a shortcut for:
+ #
+ # assert_performed_jobs 0, &block
+ def assert_no_performed_jobs(only: nil, &block)
+ assert_performed_jobs 0, only: only, &block
+ end
- # Asserts that the job passed in the block has been performed with the given arguments.
- #
- # def test_assert_performed_with
- # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do
- # MyJob.perform_later(1,2,3)
- # end
- #
- # assert_performed_with(job: MyJob, at: Date.tomorrow.noon) do
- # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
- # end
- # end
- def assert_performed_with(args = {})
- original_performed_jobs_count = performed_jobs.count
- args.assert_valid_keys(:job, :args, :at, :queue)
- serialized_args = serialize_args_for_assertion(args)
- perform_enqueued_jobs { yield }
- in_block_jobs = performed_jobs.drop(original_performed_jobs_count)
- matching_job = in_block_jobs.find do |job|
- serialized_args.all? { |key, value| value == job[key] }
- end
- assert matching_job, "No performed job found with #{args}"
- instantiate_job(matching_job)
+ # Asserts that the job passed in the block has been enqueued with the given arguments.
+ #
+ # def test_assert_enqueued_with
+ # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do
+ # MyJob.perform_later(1,2,3)
+ # end
+ #
+ # assert_enqueued_with(job: MyJob, at: Date.tomorrow.noon) do
+ # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
+ # end
+ # end
+ def assert_enqueued_with(args = {})
+ original_enqueued_jobs_count = enqueued_jobs.count
+ args.assert_valid_keys(:job, :args, :at, :queue)
+ serialized_args = serialize_args_for_assertion(args)
+ yield
+ in_block_jobs = enqueued_jobs.drop(original_enqueued_jobs_count)
+ matching_job = in_block_jobs.find do |job|
+ serialized_args.all? { |key, value| value == job[key] }
end
+ assert matching_job, "No enqueued job found with #{args}"
+ instantiate_job(matching_job)
+ end
- def perform_enqueued_jobs(only: nil)
- old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
- old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs
- old_filter = queue_adapter.filter
-
- begin
- queue_adapter.perform_enqueued_jobs = true
- queue_adapter.perform_enqueued_at_jobs = true
- queue_adapter.filter = only
- yield
- ensure
- queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs
- queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs
- queue_adapter.filter = old_filter
- end
+ # Asserts that the job passed in the block has been performed with the given arguments.
+ #
+ # def test_assert_performed_with
+ # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do
+ # MyJob.perform_later(1,2,3)
+ # end
+ #
+ # assert_performed_with(job: MyJob, at: Date.tomorrow.noon) do
+ # MyJob.set(wait_until: Date.tomorrow.noon).perform_later
+ # end
+ # end
+ def assert_performed_with(args = {})
+ original_performed_jobs_count = performed_jobs.count
+ args.assert_valid_keys(:job, :args, :at, :queue)
+ serialized_args = serialize_args_for_assertion(args)
+ perform_enqueued_jobs { yield }
+ in_block_jobs = performed_jobs.drop(original_performed_jobs_count)
+ matching_job = in_block_jobs.find do |job|
+ serialized_args.all? { |key, value| value == job[key] }
end
+ assert matching_job, "No performed job found with #{args}"
+ instantiate_job(matching_job)
+ end
+
+ def perform_enqueued_jobs(only: nil)
+ old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
+ old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs
+ old_filter = queue_adapter.filter
- def queue_adapter
- ActiveJob::Base.queue_adapter
+ begin
+ queue_adapter.perform_enqueued_jobs = true
+ queue_adapter.perform_enqueued_at_jobs = true
+ queue_adapter.filter = only
+ yield
+ ensure
+ queue_adapter.perform_enqueued_jobs = old_perform_enqueued_jobs
+ queue_adapter.perform_enqueued_at_jobs = old_perform_enqueued_at_jobs
+ queue_adapter.filter = old_filter
end
+ end
- delegate :enqueued_jobs, :enqueued_jobs=,
- :performed_jobs, :performed_jobs=,
- to: :queue_adapter
+ def queue_adapter
+ ActiveJob::Base.queue_adapter
+ end
- private
- def clear_enqueued_jobs # :nodoc:
- enqueued_jobs.clear
- end
+ private
+ def clear_enqueued_jobs # :nodoc:
+ enqueued_jobs.clear
+ end
- def clear_performed_jobs # :nodoc:
- performed_jobs.clear
- end
+ def clear_performed_jobs # :nodoc:
+ performed_jobs.clear
+ end
- def enqueued_jobs_size(only: nil) # :nodoc:
- if only
- enqueued_jobs.count { |job| Array(only).include?(job.fetch(:job)) }
- else
- enqueued_jobs.count
- end
+ def enqueued_jobs_size(only: nil) # :nodoc:
+ if only
+ enqueued_jobs.count { |job| Array(only).include?(job.fetch(:job)) }
+ else
+ enqueued_jobs.count
end
+ end
- def serialize_args_for_assertion(args) # :nodoc:
- args.dup.tap do |serialized_args|
- serialized_args[:args] = ActiveJob::Arguments.serialize(serialized_args[:args]) if serialized_args[:args]
- serialized_args[:at] = serialized_args[:at].to_f if serialized_args[:at]
- end
+ def serialize_args_for_assertion(args) # :nodoc:
+ args.dup.tap do |serialized_args|
+ serialized_args[:args] = ActiveJob::Arguments.serialize(serialized_args[:args]) if serialized_args[:args]
+ serialized_args[:at] = serialized_args[:at].to_f if serialized_args[:at]
end
+ end
- def instantiate_job(payload) # :nodoc:
- job = payload[:job].new(*payload[:args])
- job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at)
- job.queue_name = payload[:queue]
- job
- end
- end
+ def instantiate_job(payload) # :nodoc:
+ job = payload[:job].new(*payload[:args])
+ job.scheduled_at = Time.at(payload[:at]) if payload.key?(:at)
+ job.queue_name = payload[:queue]
+ job
+ end
end
end
diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb
index 2115fb9f71..6e43e4a269 100644
--- a/activejob/lib/rails/generators/job/job_generator.rb
+++ b/activejob/lib/rails/generators/job/job_generator.rb
@@ -17,7 +17,22 @@ module Rails # :nodoc:
def create_job_file
template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb")
+
+ in_root do
+ if self.behavior == :invoke && !File.exist?(application_job_file_name)
+ template 'application_job.rb', application_job_file_name
+ end
+ end
end
+
+ private
+ def application_job_file_name
+ @application_job_file_name ||= if mountable_engine?
+ "app/jobs/#{namespaced_path}/application_job.rb"
+ else
+ 'app/jobs/application_job.rb'
+ end
+ end
end
end
end
diff --git a/activejob/lib/rails/generators/job/templates/application_job.rb b/activejob/lib/rails/generators/job/templates/application_job.rb
new file mode 100644
index 0000000000..0b113b950e
--- /dev/null
+++ b/activejob/lib/rails/generators/job/templates/application_job.rb
@@ -0,0 +1,4 @@
+<% module_namespacing do -%>
+class ApplicationJob < ActiveJob::Base
+end
+<% end -%>
diff --git a/activejob/test/adapters/async.rb b/activejob/test/adapters/async.rb
index 5fcfb89566..08eb9658cd 100644
--- a/activejob/test/adapters/async.rb
+++ b/activejob/test/adapters/async.rb
@@ -1,4 +1,2 @@
-require 'active_job/async_job'
-
ActiveJob::Base.queue_adapter = :async
-ActiveJob::AsyncJob.perform_immediately!
+ActiveJob::Base.queue_adapter.immediate = true
diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb
index eb8ad185aa..59dc3d7f78 100644
--- a/activejob/test/cases/argument_serialization_test.rb
+++ b/activejob/test/cases/argument_serialization_test.rb
@@ -62,13 +62,14 @@ class ArgumentSerializationTest < ActiveSupport::TestCase
assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize [ { :a => [{ 2 => 3 }] } ]
end
+ end
- assert_raises ActiveJob::SerializationError do
- ActiveJob::Arguments.serialize [ '_aj_globalid' => 1 ]
- end
-
- assert_raises ActiveJob::SerializationError do
- ActiveJob::Arguments.serialize [ :_aj_globalid => 1 ]
+ test 'should not allow reserved hash keys' do
+ ['_aj_globalid', :_aj_globalid, '_aj_symbol_keys', :_aj_symbol_keys,
+ '_aj_hash_with_indifferent_access', :_aj_hash_with_indifferent_access].each do |key|
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [key => 1]
+ end
end
end
diff --git a/activejob/test/cases/async_job_test.rb b/activejob/test/cases/async_job_test.rb
deleted file mode 100644
index 2642cfc608..0000000000
--- a/activejob/test/cases/async_job_test.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-require 'helper'
-require 'jobs/hello_job'
-require 'jobs/queue_as_job'
-
-class AsyncJobTest < ActiveSupport::TestCase
- def using_async_adapter?
- ActiveJob::Base.queue_adapter.is_a? ActiveJob::QueueAdapters::AsyncAdapter
- end
-
- setup do
- ActiveJob::AsyncJob.perform_asynchronously!
- end
-
- teardown do
- ActiveJob::AsyncJob::QUEUES.clear
- ActiveJob::AsyncJob.perform_immediately!
- end
-
- test "#create_thread_pool returns a thread_pool" do
- thread_pool = ActiveJob::AsyncJob.create_thread_pool
- assert thread_pool.is_a? Concurrent::ExecutorService
- assert_not thread_pool.is_a? Concurrent::ImmediateExecutor
- end
-
- test "#create_thread_pool returns an ImmediateExecutor after #perform_immediately! is called" do
- ActiveJob::AsyncJob.perform_immediately!
- thread_pool = ActiveJob::AsyncJob.create_thread_pool
- assert thread_pool.is_a? Concurrent::ImmediateExecutor
- end
-
- test "enqueuing without specifying a queue uses the default queue" do
- skip unless using_async_adapter?
- HelloJob.perform_later
- assert ActiveJob::AsyncJob::QUEUES.key? 'default'
- end
-
- test "enqueuing to a queue that does not exist creates the queue" do
- skip unless using_async_adapter?
- QueueAsJob.perform_later
- assert ActiveJob::AsyncJob::QUEUES.key? QueueAsJob::MY_QUEUE.to_s
- end
-end
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
index 229517774e..fa94209889 100644
--- a/activejob/test/cases/job_serialization_test.rb
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -2,6 +2,7 @@ require 'helper'
require 'jobs/gid_job'
require 'jobs/hello_job'
require 'models/person'
+require 'json'
class JobSerializationTest < ActiveSupport::TestCase
setup do
@@ -15,18 +16,32 @@ class JobSerializationTest < ActiveSupport::TestCase
end
test 'serialize includes current locale' do
- assert_equal :en, HelloJob.new.serialize['locale']
+ assert_equal 'en', HelloJob.new.serialize['locale']
+ end
+
+ test 'serialize and deserialize are symmetric' do
+ # Round trip a job in memory only
+ h1 = HelloJob.new
+ h1.deserialize(h1.serialize)
+
+ # Now verify it's identical to a JSON round trip.
+ # We don't want any non-native JSON elements in the job hash,
+ # like symbols.
+ payload = JSON.dump(h1.serialize)
+ h2 = HelloJob.new
+ h2.deserialize(JSON.load(payload))
+ assert_equal h1.serialize, h2.serialize
end
test 'deserialize sets locale' do
job = HelloJob.new
- job.deserialize 'locale' => :es
- assert_equal :es, job.locale
+ job.deserialize 'locale' => 'es'
+ assert_equal 'es', job.locale
end
test 'deserialize sets default locale' do
job = HelloJob.new
job.deserialize({})
- assert_equal :en, job.locale
+ assert_equal 'en', job.locale
end
end
diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb
index 7e86415f48..54b6076f09 100644
--- a/activejob/test/helper.rb
+++ b/activejob/test/helper.rb
@@ -1,5 +1,3 @@
-require File.expand_path('../../../load_paths', __FILE__)
-
require 'active_job'
require 'support/job_buffer'
diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb
index d8425c9706..40f27500a5 100644
--- a/activejob/test/integration/queuing_test.rb
+++ b/activejob/test/integration/queuing_test.rb
@@ -57,13 +57,13 @@ class QueuingTest < ActiveSupport::TestCase
end
test 'should supply a provider_job_id when available for immediate jobs' do
- skip unless adapter_is?(:delayed_job, :sidekiq, :qu, :que, :queue_classic)
+ skip unless adapter_is?(:async, :delayed_job, :sidekiq, :qu, :que, :queue_classic)
test_job = TestJob.perform_later @id
assert test_job.provider_job_id, 'Provider job id should be set by provider'
end
test 'should supply a provider_job_id when available for delayed jobs' do
- skip unless adapter_is?(:delayed_job, :sidekiq, :que, :queue_classic)
+ skip unless adapter_is?(:async, :delayed_job, :sidekiq, :que, :queue_classic)
delayed_test_job = TestJob.set(wait: 1.minute).perform_later @id
assert delayed_test_job.provider_job_id, 'Provider job id should by set for delayed jobs by provider'
end
diff --git a/activejob/test/support/integration/adapters/async.rb b/activejob/test/support/integration/adapters/async.rb
index 42beb12b1f..44ab98437a 100644
--- a/activejob/test/support/integration/adapters/async.rb
+++ b/activejob/test/support/integration/adapters/async.rb
@@ -1,9 +1,10 @@
module AsyncJobsManager
def setup
ActiveJob::Base.queue_adapter = :async
+ ActiveJob::Base.queue_adapter.immediate = false
end
def clear_jobs
- ActiveJob::AsyncJob::QUEUES.clear
+ ActiveJob::Base.queue_adapter.shutdown
end
end
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
index 262ca72327..a0ef38b0b2 100644
--- a/activejob/test/support/integration/dummy_app_template.rb
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -2,7 +2,7 @@ if ENV['AJ_ADAPTER'] == 'delayed_job'
generate "delayed_job:active_record", "--quiet"
end
-rake("db:migrate")
+rails_command("db:migrate")
initializer 'activejob.rb', <<-CODE
require "#{File.expand_path("../jobs_manager.rb", __FILE__)}"
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 318e507ff1..7be8b2e522 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,3 +1,13 @@
+* Allow passing record being validated to the message proc to generate
+ customized error messages for that object using I18n helper.
+
+ *Prathamesh Sonpatki*
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* No changes.
+
+
## Rails 5.0.0.beta2 (February 01, 2016) ##
* No changes.
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index 53206580f0..1c3997b864 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.homepage = 'http://rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']
s.require_path = 'lib'
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index 6e2e5afd1b..90047c3c12 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -119,6 +119,9 @@ module ActiveModel
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
+ OPTION_NOT_GIVEN = Object.new # :nodoc:
+ private_constant :OPTION_NOT_GIVEN
+
included do
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
attribute_method_suffix '_previously_changed?', '_previous_change'
@@ -174,11 +177,10 @@ module ActiveModel
end
# Handles <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr, options = {}) #:nodoc:
- result = changes_include?(attr)
- result &&= options[:to] == __send__(attr) if options.key?(:to)
- result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
- result
+ def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc:
+ !!changes_include?(attr) &&
+ (to == OPTION_NOT_GIVEN || to == __send__(attr)) &&
+ (from == OPTION_NOT_GIVEN || from == changed_attributes[attr])
end
# Handles <tt>*_was</tt> for +method_missing+.
@@ -187,7 +189,7 @@ module ActiveModel
end
# Handles <tt>*_previously_changed?</tt> for +method_missing+.
- def attribute_previously_changed?(attr, options = {}) #:nodoc:
+ def attribute_previously_changed?(attr) #:nodoc:
previous_changes_include?(attr)
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index ea69e7549e..d106f65fa2 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -110,7 +110,7 @@ module ActiveModel
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
- messages[attribute].present?
+ messages.key?(attribute) && messages[attribute].present?
end
alias :has_key? :include?
alias :key? :include?
@@ -327,7 +327,7 @@ module ActiveModel
# # => {:base=>[{error: :name_or_email_blank}]}
def add(attribute, message = :invalid, options = {})
message = message.call if message.respond_to?(:call)
- detail = normalize_detail(attribute, message, options)
+ detail = normalize_detail(message, options)
message = normalize_message(attribute, message, options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
@@ -346,7 +346,7 @@ module ActiveModel
# # => {:name=>["can't be empty"]}
def add_on_empty(attributes, options = {})
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1
+ ActiveModel::Errors#add_on_empty is deprecated and will be removed in Rails 5.1.
To achieve the same use:
@@ -368,7 +368,7 @@ module ActiveModel
# # => {:name=>["can't be blank"]}
def add_on_blank(attributes, options = {})
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
- ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1
+ ActiveModel::Errors#add_on_blank is deprecated and will be removed in Rails 5.1.
To achieve the same use:
@@ -486,7 +486,8 @@ module ActiveModel
default: defaults,
model: @base.model_name.human,
attribute: @base.class.human_attribute_name(attribute),
- value: value
+ value: value,
+ object: @base
}.merge!(options)
I18n.translate(key, options)
@@ -502,7 +503,7 @@ module ActiveModel
end
end
- def normalize_detail(attribute, message, options)
+ def normalize_detail(message, options)
{ error: message }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
end
end
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
index 94514a0657..62862aa4e9 100644
--- a/activemodel/lib/active_model/gem_version.rb
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -8,7 +8,7 @@ module ActiveModel
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activemodel/lib/active_model/type.rb b/activemodel/lib/active_model/type.rb
index bec851594f..6ec3452478 100644
--- a/activemodel/lib/active_model/type.rb
+++ b/activemodel/lib/active_model/type.rb
@@ -47,7 +47,7 @@ module ActiveModel
register(:binary, Type::Binary)
register(:boolean, Type::Boolean)
register(:date, Type::Date)
- register(:date_time, Type::DateTime)
+ register(:datetime, Type::DateTime)
register(:decimal, Type::Decimal)
register(:float, Type::Float)
register(:immutable_string, Type::ImmutableString)
diff --git a/activemodel/lib/active_model/type/decimal.rb b/activemodel/lib/active_model/type/decimal.rb
index d19d8baada..11ea327026 100644
--- a/activemodel/lib/active_model/type/decimal.rb
+++ b/activemodel/lib/active_model/type/decimal.rb
@@ -29,12 +29,12 @@ module ActiveModel
end
end
- scale ? casted_value.round(scale) : casted_value
+ apply_scale(casted_value)
end
def convert_float_to_big_decimal(value)
if precision
- BigDecimal(value, float_precision)
+ BigDecimal(apply_scale(value), float_precision)
else
value.to_d
end
@@ -47,6 +47,14 @@ module ActiveModel
precision.to_i
end
end
+
+ def apply_scale(value)
+ if scale
+ value.round(scale)
+ else
+ value
+ end
+ end
end
end
end
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index bad9e4f9a9..d49af603bb 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -30,14 +30,15 @@ module ActiveModel
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # In Ruby 2.2 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
# possible values in the range for equality, which is slower but more accurate.
# <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
- # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges.
+ # endpoints, which is fast but is only accurate on Numeric, Time, Date,
+ # or DateTime ranges.
def inclusion_method(enumerable)
if enumerable.is_a? Range
case enumerable.first
- when Numeric, Time, DateTime
+ when Numeric, Time, DateTime, Date
:cover?
else
:include?
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index 910cca2f49..79297ac119 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -136,7 +136,7 @@ module ActiveModel
# * <tt>:too_long</tt> - The error message if the attribute goes over the
# maximum (default is: "is too long (maximum is %{count} characters)").
# * <tt>:too_short</tt> - The error message if the attribute goes under the
- # minimum (default is: "is too short (min is %{count} characters)").
+ # minimum (default is: "is too short (minimum is %{count} characters)").
# * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt>
# method and the attribute is the wrong size (default is: "is the wrong
# length (should be %{count} characters)").
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index 1d2888a818..109bf038b0 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -163,10 +163,6 @@ module ActiveModel
# +ArgumentError+ when invalid options are supplied.
def check_validity!
end
-
- def should_validate?(record) # :nodoc:
- !record.persisted? || record.changed? || record.marked_for_destruction?
- end
end
# +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index a5ac055033..26ba6ba4fb 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -128,6 +128,13 @@ class ErrorsTest < ActiveModel::TestCase
assert !person.errors.include?(:foo)
end
+ test "include? does not add a key to messages hash" do
+ person = Person.new
+ person.errors.include?(:foo)
+
+ assert_not person.errors.messages.key?(:foo)
+ end
+
test "adding errors using conditionals with Person#validate!" do
person = Person.new
person.validate!
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index 27fdbc739c..44c26e4f10 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -1,5 +1,3 @@
-require File.expand_path('../../../../load_paths', __FILE__)
-
require 'active_model'
require 'active_support/core_ext/string/access'
diff --git a/activemodel/test/cases/type/decimal_test.rb b/activemodel/test/cases/type/decimal_test.rb
index 353dbf84ad..1950566c0e 100644
--- a/activemodel/test/cases/type/decimal_test.rb
+++ b/activemodel/test/cases/type/decimal_test.rb
@@ -52,6 +52,13 @@ module ActiveModel
assert_not type.changed?(5.0, 5.0, '5.0')
assert_not type.changed?(-5.0, -5.0, '-5.0')
end
+
+ def test_scale_is_applied_before_precision_to_prevent_rounding_errors
+ type = Decimal.new(precision: 5, scale: 3)
+
+ assert_equal BigDecimal("1.250"), type.cast(1.250473853637869)
+ assert_equal BigDecimal("1.250"), type.cast("1.250473853637869")
+ end
end
end
end
diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb
index 86bbbe6ebe..ea4c2ee7df 100644
--- a/activemodel/test/cases/validations/format_validation_test.rb
+++ b/activemodel/test/cases/validations/format_validation_test.rb
@@ -73,7 +73,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validate_format_of_with_multiline_regexp_and_option
- assert_nothing_raised(ArgumentError) do
+ assert_nothing_raised do
Topic.validates_format_of(:title, with: /^Valid Title$/, multiline: true)
end
end
diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb
index 55d1fb4dcb..9bd44175a6 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -21,24 +21,38 @@ class InclusionValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_time_range
- Topic.validates_inclusion_of(:created_at, in: 1.year.ago..Time.now)
+ range_begin = 1.year.ago
+ range_end = Time.now
+ Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
assert Topic.new(title: 'aaa', created_at: 2.years.ago).invalid?
assert Topic.new(title: 'aaa', created_at: 3.months.ago).valid?
assert Topic.new(title: 'aaa', created_at: 37.weeks.from_now).invalid?
+ assert Topic.new(title: 'aaa', created_at: range_begin).valid?
+ assert Topic.new(title: 'aaa', created_at: range_end).valid?
end
def test_validates_inclusion_of_date_range
- Topic.validates_inclusion_of(:created_at, in: 1.year.until(Date.today)..Date.today)
+ range_begin = 1.year.until(Date.today)
+ range_end = Date.today
+ Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
assert Topic.new(title: 'aaa', created_at: 2.years.until(Date.today)).invalid?
assert Topic.new(title: 'aaa', created_at: 3.months.until(Date.today)).valid?
assert Topic.new(title: 'aaa', created_at: 37.weeks.since(Date.today)).invalid?
+ assert Topic.new(title: 'aaa', created_at: 1.year.until(Date.today)).valid?
+ assert Topic.new(title: 'aaa', created_at: Date.today).valid?
+ assert Topic.new(title: 'aaa', created_at: range_begin).valid?
+ assert Topic.new(title: 'aaa', created_at: range_end).valid?
end
def test_validates_inclusion_of_date_time_range
- Topic.validates_inclusion_of(:created_at, in: 1.year.until(DateTime.current)..DateTime.current)
+ range_begin = 1.year.until(DateTime.current)
+ range_end = DateTime.current
+ Topic.validates_inclusion_of(:created_at, in: range_begin..range_end)
assert Topic.new(title: 'aaa', created_at: 2.years.until(DateTime.current)).invalid?
assert Topic.new(title: 'aaa', created_at: 3.months.until(DateTime.current)).valid?
assert Topic.new(title: 'aaa', created_at: 37.weeks.since(DateTime.current)).invalid?
+ assert Topic.new(title: 'aaa', created_at: range_begin).valid?
+ assert Topic.new(title: 'aaa', created_at: range_end).valid?
end
def test_validates_inclusion_of
@@ -58,9 +72,9 @@ class InclusionValidationTest < ActiveModel::TestCase
assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) }
assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: 0) }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: "hi!") }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: {}) }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: []) }
+ assert_nothing_raised { Topic.validates_inclusion_of(:title, in: "hi!") }
+ assert_nothing_raised { Topic.validates_inclusion_of(:title, in: {}) }
+ assert_nothing_raised { Topic.validates_inclusion_of(:title, in: []) }
end
def test_validates_inclusion_of_with_allow_nil
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index f0317ad219..2a4e9f842f 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -444,4 +444,20 @@ class ValidationsTest < ActiveModel::TestCase
assert topic.invalid?
assert duped.valid?
end
+
+ def test_validation_with_message_as_proc_that_takes_a_record_as_a_parameter
+ Topic.validates_presence_of(:title, message: proc { |record| "You have failed me for the last time, #{record.author_name}." })
+
+ t = Topic.new(author_name: 'Admiral')
+ assert t.invalid?
+ assert_equal ["You have failed me for the last time, Admiral."], t.errors[:title]
+ end
+
+ def test_validation_with_message_as_proc_that_takes_record_and_data_as_a_parameters
+ Topic.validates_presence_of(:title, message: proc { |record, data| "#{data[:attribute]} is missing. You have failed me for the last time, #{record.author_name}." })
+
+ t = Topic.new(author_name: 'Admiral')
+ assert t.invalid?
+ assert_equal ["Title is missing. You have failed me for the last time, Admiral."], t.errors[:title]
+ end
end
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index ab17a2b438..16ce131cf4 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,6 +1,239 @@
+* SQLite: Fix uniqueness validation when values exceed the column limit.
+
+ SQLite doesn't impose length restrictions on strings, BLOBs, or numeric
+ values. It treats them as helpful metadata. When we truncate strings
+ before checking uniqueness, we'd miss values that exceed the column limit.
+
+ Other databases enforce length limits. A large value will pass uniqueness
+ validation since the column limit guarantees no value that long exists.
+ When we insert the row, it'll raise `ActiveRecord::ValueTooLong` as we
+ expect.
+
+ This fixes edge-case incorrect validation failures for values that exceed
+ the column limit but are identical to an existing value *when truncated*.
+ Now these will pass validation and raise an exception.
+
+ *Ryuta Kamizono*
+
+* Raise `ActiveRecord::ValueTooLong` when column limits are exceeded.
+ Supported by MySQL and PostgreSQL adapters.
+
+ *Ryuta Kamizono*
+
+* Migrations: `#foreign_key` respects `table_name_prefix` and `_suffix`.
+
+ *Ryuta Kamizono*
+
+* SQLite: Force NOT NULL primary keys.
+
+ From SQLite docs: https://www.sqlite.org/lang_createtable.html
+ According to the SQL standard, PRIMARY KEY should always imply NOT
+ NULL. Unfortunately, due to a bug in some early versions, this is not
+ the case in SQLite. Unless the column is an INTEGER PRIMARY KEY or the
+ table is a WITHOUT ROWID table or the column is declared NOT NULL,
+ SQLite allows NULL values in a PRIMARY KEY column. SQLite could be
+ fixed to conform to the standard, but doing so might break legacy
+ applications. Hence, it has been decided to merely document the fact
+ that SQLite allowing NULLs in most PRIMARY KEY columns.
+
+ Now we override column options to explicitly set NOT NULL rather than rely
+ on implicit NOT NULL like MySQL and PostgreSQL adapters.
+
+ *Ryuta Kamizono*
+
+* Added notice when a database is successfully created or dropped.
+
+ Example:
+
+ $ bin/rails db:create
+ Created database 'blog_development'
+ Created database 'blog_test'
+
+ $ bin/rails db:drop
+ Dropped database 'blog_development'
+ Dropped database 'blog_test'
+
+ Changed older notices
+ `blog_development already exists` to `Database 'blog_development' already exists`.
+ and
+ `Couldn't drop blog_development` to `Couldn't drop database 'blog_development'`.
+
+ *bogdanvlviv*
+
+* Database comments. Annotate database objects (tables, columns, indexes)
+ with comments stored in database metadata. PostgreSQL & MySQL support.
+
+ create_table :pages, force: :cascade, comment: 'CMS content pages' do |t|
+ t.string :path, comment: 'Path fragment of page URL used for routing'
+ t.string :locale, comment: 'RFC 3066 locale code of website language section'
+ t.index [:path, :locale], comment: 'Look up pages by URI'
+ end
+
+ *Andrey Novikov*
+
+* Add `quoted_time` for truncating the date part of a TIME column value.
+ This fixes queries on TIME column on MariaDB, as it doesn't ignore the
+ date part of the string when it coerces to time.
+
+ *Ryuta Kamizono*
+
+* Properly accept all valid JSON primitives in the JSON data type.
+
+ Fixes #24234
+
+ *Sean Griffin*
+
+* MariaDB 5.3+ supports microsecond datetime precision.
+
+ *Jeremy Daer*
+
+* Delegate `empty?`, `none?` and `one?`. Now they can be invoked as model class methods.
+
+ Example:
+
+ # When no record is found on the table
+ Topic.empty? # => true
+ Topic.none? # => true
+
+ # When only one record is found on the table
+ Topic.one? # => true
+
+ *Kenta Shirai*
+
+* The form builder now properly displays values when passing a proc form
+ default to the attributes API.
+
+ Fixes #24249.
+
+ *Sean Griffin*
+
+* The schema cache is now cleared after the `db:migrate` task is run.
+
+ Closes #24273.
+
+ *Chris Arcand*
+
+* MySQL: strict mode respects other SQL modes rather than overwriting them.
+ Setting `strict: true` adds `STRICT_ALL_TABLES` to `sql_mode`. Setting
+ `strict: false` removes `STRICT_TRANS_TABLES`, `STRICT_ALL_TABLES`, and
+ `TRADITIONAL` from `sql_mode`.
+
+ *Ryuta Kamizono*
+
+* Execute default_scope defined by abstract class in the context of subclass.
+
+ Fixes #23413.
+ Fixes #10658.
+
+ *Mehmet Emin İNAÇ*
+
+* Fix an issue when preloading associations with extensions.
+ Previously every association with extension methods was transformed into an
+ instance dependent scope. This is no longer the case.
+
+ Fixes #23934.
+
+ *Yves Senn*
+
+* Deprecate `{insert|update|delete}_sql` in `DatabaseStatements`.
+ Use the `{insert|update|delete}` public methods instead.
+
+ *Ryuta Kamizono*
+
+* Added a configuration option to have active record raise an ArgumentError
+ if the order or limit is ignored in a batch query, rather than logging a
+ warning message.
+
+ *Scott Ringwelski*
+
+* Honour the order of the joining model in a `has_many :through` association when eager loading.
+
+ Example:
+
+ The below will now follow the order of `by_lines` when eager loading `authors`.
+
+ class Article < ActiveRecord::Base
+ has_many :by_lines, -> { order(:position) }
+ has_many :authors, through: :by_lines
+ end
+
+ Fixes #17864.
+
+ *Yasyf Mohamedali*, *Joel Turkel*
+
+* Ensure that the Suppressor runs before validations.
+
+ This moves the suppressor up to be run before validations rather than after
+ validations. There's no reason to validate a record you aren't planning on saving.
+
+ *Eileen M. Uchitelle*
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* Save many-to-many objects based on association primary key.
+
+ Fixes #20995.
+
+ *himesh-r*
+
+* Ensure that mutations of the array returned from `ActiveRecord::Relation#to_a`
+ do not affect the original relation, by returning a duplicate array each time.
+
+ This brings the behavior in line with `CollectionProxy#to_a`, which was
+ already more careful.
+
+ *Matthew Draper*
+
+* Fixed `where` for polymorphic associations when passed an array containing different types.
+
+ Fixes #17011.
+
+ Example:
+
+ PriceEstimate.where(estimate_of: [Treasure.find(1), Car.find(2)])
+ # => SELECT "price_estimates".* FROM "price_estimates"
+ WHERE (("price_estimates"."estimate_of_type" = 'Treasure' AND "price_estimates"."estimate_of_id" = 1)
+ OR ("price_estimates"."estimate_of_type" = 'Car' AND "price_estimates"."estimate_of_id" = 2))
+
+ *Philippe Huibonhoa*
+
+* Fix a bug where using `t.foreign_key` twice with the same `to_table` within
+ the same table definition would only create one foreign key.
+
+ *George Millo*
+
+* Fix a regression on has many association, where calling a child from parent in child's callback
+ results in same child records getting added repeatedly to target.
+
+ Fixes #13387.
+
+ *Bogdan Gusiev*, *Jon Hinson*
+
+* Rework `ActiveRecord::Relation#last`.
+
+ 1. Never perform additional SQL on loaded relation
+ 2. Use SQL reverse order instead of loading relation if relation doesn't have limit
+ 3. Deprecated relation loading when SQL order can not be automatically reversed
+
+ Topic.order("title").load.last(3)
+ # before: SELECT ...
+ # after: No SQL
+
+ Topic.order("title").last
+ # before: SELECT * FROM `topics`
+ # after: SELECT * FROM `topics` ORDER BY `topics`.`title` DESC LIMIT 1
+
+ Topic.order("coalesce(author, title)").last
+ # before: SELECT * FROM `topics`
+ # after: Deprecation Warning for irreversible order
+
+ *Bogdan Gusiev*
+
* Allow `joins` to be unscoped.
- Closes #13775.
+ Fixes #13775.
+
+ *Takashi Kokubun*
* Add ActiveRecord `#second_to_last` and `#third_to_last` methods.
@@ -124,6 +357,21 @@
## Rails 5.0.0.beta1 (December 18, 2015) ##
+* Limit record touching to once per transaction.
+
+ If you have a parent/grand-parent relation like:
+
+ Comment belongs_to :message, touch: true
+ Message belongs_to :project, touch: true
+ Project belongs_to :account, touch: true
+
+ When the lowest entry(`Comment`) is saved, now, it won't repeat the touch
+ call multiple times for the parent records.
+
+ Related #18606.
+
+ *arthurnn*
+
* Order the result of `find(ids)` to match the passed array, if the relation
has no explicit order defined.
@@ -398,13 +646,13 @@
* Add option to index errors in nested attributes
For models which have nested attributes, errors within those models will
- now be indexed if :index_errors is specified when defining a
+ now be indexed if `:index_errors` is specified when defining a
has_many relationship, or if its set in the global config.
Example:
class Guitar < ActiveRecord::Base
- has_many :tuning_pegs
+ has_many :tuning_pegs, index_errors: true
accepts_nested_attributes_for :tuning_pegs
end
@@ -620,7 +868,7 @@
*Ben Murphy*, *Matthew Draper*
-* `bin/rake db:migrate` uses
+* `bin/rails db:migrate` uses
`ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of
`Migrator.migrations_paths`.
@@ -1282,11 +1530,6 @@
*Hyonjee Joo*
-* Deprecate passing of `start` value to `find_in_batches` and `find_each`
- in favour of `begin_at` value.
-
- *Vipul A M*
-
* Add `foreign_key_exists?` method.
*Tõnis Simo*
@@ -1396,18 +1639,6 @@
*Chris Sinjakli*
-* Validation errors would be raised for parent records when an association
- was saved when the parent had `validate: false`. It should not be the
- responsibility of the model to validate an associated object unless the
- object was created or modified by the parent.
-
- This fixes the issue by skipping validations if the parent record is
- persisted, not changed, and not marked for destruction.
-
- Fixes #17621.
-
- *Eileen M. Uchitelle*, *Aaron Patterson*
-
* Fix n+1 query problem when eager loading nil associations (fixes #18312)
*Sammy Larbi*
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
index a74fcf2df7..cd22f76d01 100644
--- a/activerecord/RUNNING_UNIT_TESTS.rdoc
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -7,11 +7,11 @@ http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#setting-up-
To run a specific test:
- $ ruby -Itest test/cases/base_test.rb -n method_name
+ $ bundle exec ruby -Itest test/cases/base_test.rb -n method_name
To run a set of tests:
- $ ruby -Itest test/cases/base_test.rb
+ $ bundle exec ruby -Itest test/cases/base_test.rb
You can also run tests that depend upon a specific database backend. For
example:
@@ -41,7 +41,7 @@ parameters in +test/config.example.yml+ are.
You can override the +connections:+ parameter in either file using the +ARCONN+
(Active Record CONNection) environment variable:
- $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb
+ $ ARCONN=postgresql bundle exec ruby -Itest test/cases/base_test.rb
You can specify a custom location for the config file using the +ARCONFIG+
environment variable.
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 4405da2812..881dee13e4 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.homepage = 'http://rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'examples/**/*', 'lib/**/*']
s.require_path = 'lib'
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
index a5a1f284a0..c3c46c19c5 100644
--- a/activerecord/examples/performance.rb
+++ b/activerecord/examples/performance.rb
@@ -1,4 +1,3 @@
-require File.expand_path('../../../load_paths', __FILE__)
require "active_record"
require 'benchmark/ips'
diff --git a/activerecord/examples/simple.rb b/activerecord/examples/simple.rb
index 4ed5d80eb2..5a041e8417 100644
--- a/activerecord/examples/simple.rb
+++ b/activerecord/examples/simple.rb
@@ -1,4 +1,3 @@
-require File.expand_path('../../../load_paths', __FILE__)
require 'active_record'
class Person < ActiveRecord::Base
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index ab3846ae65..baa497dc98 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -137,7 +137,6 @@ module ActiveRecord
eager_autoload do
autoload :AbstractAdapter
- autoload :ConnectionManagement, "active_record/connection_adapters/abstract/connection_pool"
end
end
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
index ee0bb8fafe..c18e88e4cf 100644
--- a/activerecord/lib/active_record/association_relation.rb
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -10,7 +10,7 @@ module ActiveRecord
end
def ==(other)
- other == to_a
+ other == records
end
def build(*args, &block)
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index e13fe33b85..2fbecb7d04 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -504,7 +504,7 @@ module ActiveRecord
#
# == Customizing the query
#
- # \Associations are built from <tt>Relation</tt>s, and you can use the Relation syntax
+ # \Associations are built from <tt>Relation</tt> objects, and you can use the Relation syntax
# to customize them. For example, to add a condition:
#
# class Blog < ActiveRecord::Base
@@ -1326,7 +1326,8 @@ module ActiveRecord
# Specifies type of the source association used by #has_many <tt>:through</tt> queries where the source
# association is a polymorphic #belongs_to.
# [:validate]
- # If +false+, don't validate the associated objects when saving the parent object. true by default.
+ # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default.
+ # If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
# [:autosave]
# If true, always save the associated objects or destroy them if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated objects.
@@ -1456,7 +1457,8 @@ module ActiveRecord
# Specifies type of the source association used by #has_one <tt>:through</tt> queries where the source
# association is a polymorphic #belongs_to.
# [:validate]
- # If +false+, don't validate the associated object when saving the parent object. +false+ by default.
+ # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default.
+ # If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated object.
@@ -1580,7 +1582,8 @@ module ActiveRecord
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
# [:validate]
- # If +false+, don't validate the associated objects when saving the parent object. +false+ by default.
+ # When set to +true+, validates new objects added to association when saving the parent object. +false+ by default.
+ # If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
# [:autosave]
# If true, always save the associated object or destroy it if marked for destruction, when
# saving the parent object.
@@ -1766,7 +1769,8 @@ module ActiveRecord
# So if a Person class makes a #has_and_belongs_to_many association to Project,
# the association will use "project_id" as the default <tt>:association_foreign_key</tt>.
# [:validate]
- # If +false+, don't validate the associated objects when saving the parent object. +true+ by default.
+ # When set to +true+, validates new objects added to association when saving the parent object. +true+ by default.
+ # If you want to ensure associated objects are revalidated on every update, use +validates_associated+.
# [:autosave]
# If true, always save the associated objects or destroy them if marked for destruction, when
# saving the parent object.
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index d64ab64c99..f7edfbfb5f 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -257,7 +257,7 @@ module ActiveRecord
# Returns true if statement cache should be skipped on the association reader.
def skip_statement_cache?
- reflection.scope_chain.any?(&:any?) ||
+ reflection.has_scope? ||
scope.eager_loading? ||
klass.scope_attributes? ||
reflection.source_reflection.active_record.default_scopes.any?
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 56a8dc4e18..f25bd7ca9f 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -70,7 +70,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.wrap_scope(scope, mod)
if scope
- proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ if scope.arity > 0
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { instance_exec(&scope).extending(mod) }
+ end
else
proc { extending(mod) }
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index b888148841..5fbd79d118 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -76,6 +76,9 @@ module ActiveRecord::Associations::Builder # :nodoc:
left_model.retrieve_connection
end
+ def self.primary_key
+ false
+ end
}
join_model.name = "HABTM_#{association_name.to_s.camelize}"
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 2a9627a474..b9aed05135 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -979,6 +979,10 @@ module ActiveRecord
end
alias_method :to_a, :to_ary
+ def records # :nodoc:
+ load_target
+ 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.
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 0e4e951269..b94feeff12 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -228,7 +228,7 @@ module ActiveRecord
def find_reflection(klass, name)
klass._reflect_on_association(name) or
- raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
+ raise ConfigurationError, "Can't join '#{ klass.name }' to association named '#{ name }'; perhaps you misspelled it?"
end
def build(associations, base_klass)
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index ecf6fb8643..e64af84e1a 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -184,6 +184,7 @@ module ActiveRecord
def self.new(klass, owners, reflection, preload_scope); self; end
def self.run(preloader); end
def self.preloaded_records; []; end
+ def self.owners; []; end
end
# Returns a class containing the logic needed to load preload the data
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 6c83058202..b0203909ce 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -38,12 +38,7 @@ module ActiveRecord
}
end
- record_offset = {}
- @preloaded_records.each_with_index do |record,i|
- record_offset[record] = i
- end
-
- through_records.each_with_object({}) { |(lhs,center),records_by_owner|
+ through_records.each_with_object({}) do |(lhs,center), records_by_owner|
pl_to_middle = center.group_by { |record| middle_to_pl[record] }
records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
@@ -53,13 +48,25 @@ module ActiveRecord
target_records_from_association(association)
}.compact
- rhs_records.sort_by { |rhs| record_offset[rhs] }
+ # Respect the order on `reflection_scope` if it exists, else use the natural order.
+ if reflection_scope.values[:order].present?
+ @id_map ||= id_to_index_map @preloaded_records
+ rhs_records.sort_by { |rhs| @id_map[rhs] }
+ else
+ rhs_records
+ end
end
- }
+ end
end
private
+ def id_to_index_map(ids)
+ id_map = {}
+ ids.each_with_index { |id, index| id_map[id] = index }
+ id_map
+ end
+
def reset_association(owners, association_name)
should_reset = (through_scope != through_reflection.klass.unscoped) ||
(reflection.options[:source_type] && through_reflection.collection?)
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index c7cc48ba16..f913f0852a 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -45,7 +45,7 @@ module ActiveRecord
end
def get_records
- return scope.limit(1).to_a if skip_statement_cache?
+ return scope.limit(1).records if skip_statement_cache?
conn = klass.connection
sc = reflection.association_scope_cache(conn, owner) do
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb
index 6dbd92ce28..4580813364 100644
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ b/activerecord/lib/active_record/attribute/user_provided_default.rb
@@ -4,20 +4,25 @@ module ActiveRecord
class Attribute # :nodoc:
class UserProvidedDefault < FromUser # :nodoc:
def initialize(name, value, type, database_default)
+ @user_provided_value = value
super(name, value, type, database_default)
end
- def type_cast(value)
- if value.is_a?(Proc)
- super(value.call)
+ def value_before_type_cast
+ if user_provided_value.is_a?(Proc)
+ @memoized_value_before_type_cast ||= user_provided_value.call
else
- super
+ @user_provided_value
end
end
def with_type(type)
- self.class.new(name, value_before_type_cast, type, original_attribute)
+ self.class.new(name, user_provided_value, type, original_attribute)
end
+
+ protected
+
+ attr_reader :user_provided_value
end
end
end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index a6d81c82b4..4c22be8235 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -29,14 +29,6 @@ module ActiveRecord
assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty?
end
- # Tries to assign given value to given attribute.
- # In case of an error, re-raises with the ActiveRecord constant.
- def _assign_attribute(k, v) # :nodoc:
- super
- rescue ActiveModel::UnknownAttributeError
- raise UnknownAttributeError.new(self, k)
- end
-
# Assign any deferred nested attributes after the base attributes have been set.
def assign_nested_parameter_attributes(pairs)
pairs.each { |k, v| _assign_attribute(k, v) }
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index ebaaa54b2b..e160460286 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -104,7 +104,7 @@ module ActiveRecord
To silence this deprecation warning, add the following:
- config.active_record.time_zone_aware_types << :time
+ config.active_record.time_zone_aware_types = [:datetime, :time]
MESSAGE
end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index 5d0405c3be..e0ceafc617 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -119,7 +119,7 @@ module ActiveRecord
#
# class MoneyType < ActiveRecord::Type::Integer
# def cast(value)
- # if !value.kind_of(Numeric) && value.include?('$')
+ # if !value.kind_of?(Numeric) && value.include?('$')
# price_in_dollars = value.gsub(/\$/, '').to_f
# super(price_in_dollars * 100)
# else
@@ -154,7 +154,7 @@ module ActiveRecord
# end
#
# class MoneyType < Type::Value
- # def initialize(currency_converter)
+ # def initialize(currency_converter:)
# @currency_converter = currency_converter
# end
#
@@ -171,7 +171,7 @@ module ActiveRecord
#
# class Product < ActiveRecord::Base
# currency_converter = ConversionRatesFromTheInternet.new
- # attribute :price_in_bitcoins, :money, currency_converter
+ # attribute :price_in_bitcoins, :money, currency_converter: currency_converter
# end
#
# Product.where(price_in_bitcoins: Money.new(5, "USD"))
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index bac5a38a5d..06c7482bf9 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -22,7 +22,7 @@ module ActiveRecord
#
# == Validation
#
- # Children records are validated unless <tt>:validate</tt> is +false+.
+ # Child records are validated unless <tt>:validate</tt> is +false+.
#
# == Callbacks
#
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 7ed2fe48be..6a1a27ce41 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -169,7 +169,8 @@ module ActiveRecord #:nodoc:
# ActiveRecord::RecordNotFound error if they do not return any records,
# like <tt>Person.find_by_last_name!</tt>.
#
- # It's also possible to use multiple attributes in the same find by separating them with "_and_".
+ # It's also possible to use multiple attributes in the same <tt>find_by_</tt> by separating them with
+ # "_and_".
#
# Person.find_by(user_name: user_name, password: password)
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 854f9776a3..1f1b11eb68 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -179,7 +179,7 @@ module ActiveRecord
#
# If the +before_validation+ callback throws +:abort+, the process will be
# aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+.
- # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise a ActiveRecord::RecordInvalid exception.
+ # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an ActiveRecord::RecordInvalid exception.
# Nothing will be appended to the errors object.
#
# == Canceling callbacks
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index ccd2899489..e389d818fd 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -951,24 +951,5 @@ module ActiveRecord
owner_to_pool && owner_to_pool[owner.name]
end
end
-
- class ConnectionManagement
- def initialize(app)
- @app = app
- end
-
- def call(env)
- testing = env['rack.test']
-
- status, headers, body = @app.call(env)
- proxy = ::Rack::BodyProxy.new(body) do
- ActiveRecord::Base.clear_active_connections! unless testing
- end
- [status, headers, proxy]
- rescue Exception
- ActiveRecord::Base.clear_active_connections! unless testing
- raise
- end
- end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index bb5119d64e..824040775d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -30,7 +30,7 @@ module ActiveRecord
def select_all(arel, name = nil, binds = [], preparable: nil)
arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
- if arel.is_a?(String) && preparable.nil?
+ if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
preparable = false
else
preparable = visitor.preparable
@@ -125,18 +125,21 @@ module ActiveRecord
end
alias create insert
alias insert_sql insert
+ deprecate insert_sql: :insert
# Executes the update statement and returns the number of rows affected.
def update(arel, name = nil, binds = [])
exec_update(to_sql(arel, binds), name, binds)
end
alias update_sql update
+ deprecate update_sql: :update
# Executes the delete statement and returns the number of rows affected.
def delete(arel, name = nil, binds = [])
exec_delete(to_sql(arel, binds), name, binds)
end
alias delete_sql delete
+ deprecate delete_sql: :delete
# Returns +true+ when the connection adapter supports prepared statement
# caching, otherwise returns +false+
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 33dbab41cb..0bdfd4f900 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -65,7 +65,7 @@ module ActiveRecord
if @query_cache_enabled && !locked?(arel)
arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
- cache_sql(sql, binds) { super(sql, name, binds, preparable: visitor.preparable) }
+ cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) }
else
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 7e3760d34b..860ef17dca 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -82,7 +82,7 @@ module ActiveRecord
# Quotes the column name. Defaults to no quoting.
def quote_column_name(column_name)
- column_name
+ column_name.to_s
end
# Quotes the table name. Defaults to column name quoting.
@@ -146,6 +146,10 @@ module ActiveRecord
end
end
+ def quoted_time(value) # :nodoc:
+ quoted_date(value).sub(/\A2000-01-01 /, '')
+ end
+
def prepare_binds_for_database(binds) # :nodoc:
binds.map(&:value_for_database)
end
@@ -166,6 +170,7 @@ module ActiveRecord
# BigDecimals need to be put in a non-normalized form and quoted.
when BigDecimal then value.to_s('F')
when Numeric, ActiveSupport::Duration then value.to_s
+ when Type::Time::Value then "'#{quoted_time(value)}'"
when Date, Time then "'#{quoted_date(value)}'"
when Symbol then "'#{quote_string(value.to_s)}'"
when Class then "'#{value}'"
@@ -181,6 +186,7 @@ module ActiveRecord
when false then unquoted_false
# BigDecimals need to be put in a non-normalized form and quoted.
when BigDecimal then value.to_s('F')
+ when Type::Time::Value then quoted_time(value)
when Date, Time then quoted_date(value)
when *types_which_need_no_typecasting
value
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 0ba4d94e3c..6add697eeb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -53,8 +53,8 @@ module ActiveRecord
statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) })
end
- create_sql << "(#{statements.join(', ')}) " if statements.present?
- create_sql << "#{o.options}"
+ create_sql << "(#{statements.join(', ')})" if statements.present?
+ add_table_options!(create_sql, table_options(o))
create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
create_sql
end
@@ -82,6 +82,19 @@ module ActiveRecord
"DROP CONSTRAINT #{quote_column_name(name)}"
end
+ def table_options(o)
+ table_options = {}
+ table_options[:comment] = o.comment
+ table_options[:options] = o.options
+ table_options
+ end
+
+ def add_table_options!(create_sql, options)
+ if options_sql = options[:options]
+ create_sql << " #{options_sql}"
+ end
+ end
+
def column_options(o)
column_options = {}
column_options[:null] = o.null unless o.null.nil?
@@ -92,6 +105,7 @@ module ActiveRecord
column_options[:auto_increment] = o.auto_increment
column_options[:primary_key] = o.primary_key
column_options[:collation] = o.collation
+ column_options[:comment] = o.comment
column_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 cb10ca9929..bbb0e9249d 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -3,14 +3,14 @@ module ActiveRecord
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc:
def primary_key?
primary_key || type.to_sym == :primary_key
@@ -207,17 +207,18 @@ module ActiveRecord
include ColumnMethods
attr_accessor :indexes
- attr_reader :name, :temporary, :options, :as, :foreign_keys
+ attr_reader :name, :temporary, :options, :as, :foreign_keys, :comment
- def initialize(name, temporary, options, as = nil)
+ def initialize(name, temporary = false, options = nil, as = nil, comment: nil)
@columns_hash = {}
@indexes = {}
- @foreign_keys = {}
+ @foreign_keys = []
@primary_keys = nil
@temporary = temporary
@options = options
@as = as
@name = name
+ @comment = comment
end
def primary_keys(name = nil) # :nodoc:
@@ -330,7 +331,10 @@ module ActiveRecord
end
def foreign_key(table_name, options = {}) # :nodoc:
- foreign_keys[table_name] = options
+ 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])
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
@@ -373,6 +377,7 @@ module ActiveRecord
column.auto_increment = options[:auto_increment]
column.primary_key = type == :primary_key || options[:primary_key]
column.collation = options[:collation]
+ column.comment = options[:comment]
column
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index b1b6044e72..6b5fad00ab 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -7,15 +7,16 @@ module ActiveRecord
# Adapter level by over-writing this code inside the database specific adapters
module ColumnDumper
def column_spec(column)
- spec = prepare_column_options(column)
- (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")}
+ spec = Hash[prepare_column_options(column).map { |k, v| [k, "#{k}: #{v}"] }]
+ spec[:name] = column.name.inspect
+ spec[:type] = schema_type(column).to_s
spec
end
def column_spec_for_primary_key(column)
- return if column.type == :integer
+ return {} if default_primary_key?(column)
spec = { id: schema_type(column).inspect }
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
+ spec.merge!(prepare_column_options(column).except!(:null))
end
# This can be overridden on an Adapter level basis to support other
@@ -23,9 +24,6 @@ module ActiveRecord
# PostgreSQL::ColumnDumper)
def prepare_column_options(column)
spec = {}
- spec[:name] = column.name.inspect
- spec[:type] = schema_type(column).to_s
- spec[:null] = 'false' unless column.null
if limit = schema_limit(column)
spec[:limit] = limit
@@ -42,26 +40,38 @@ module ActiveRecord
default = schema_default(column) if column.has_default?
spec[:default] = default unless default.nil?
+ spec[:null] = 'false' unless column.null
+
if collation = schema_collation(column)
spec[:collation] = collation
end
+ spec[:comment] = column.comment.inspect if column.comment
+
spec
end
# Lists the valid migration options
def migration_keys
- [:name, :limit, :precision, :scale, :default, :null, :collation]
+ [:name, :limit, :precision, :scale, :default, :null, :collation, :comment]
end
private
+ def default_primary_key?(column)
+ schema_type(column) == :integer
+ end
+
def schema_type(column)
- column.type
+ if column.bigint?
+ :bigint
+ else
+ column.type
+ end
end
def schema_limit(column)
- limit = column.limit
+ limit = column.limit unless column.bigint?
limit.inspect if limit && limit != native_database_types[column.type][:limit]
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index f0f855963a..4f361fed6b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -18,6 +18,11 @@ module ActiveRecord
nil
end
+ # Returns the table comment that's stored in database metadata.
+ def table_comment(table_name)
+ nil
+ end
+
# Truncates a table alias according to the limits of the current adapter.
def table_alias_for(table_name)
table_name[0...table_alias_length].tr('.', '_')
@@ -254,8 +259,8 @@ module ActiveRecord
# SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
#
# See also TableDefinition#column for details on how to create columns.
- def create_table(table_name, options = {})
- td = create_table_definition table_name, options[:temporary], options[:options], options[:as]
+ def create_table(table_name, comment: nil, **options)
+ td = create_table_definition table_name, options[:temporary], options[:options], options[:as], comment: comment
if options[:id] != false && !options[:as]
pk = options.fetch(:primary_key) do
@@ -283,6 +288,14 @@ module ActiveRecord
end
end
+ if supports_comments? && !supports_comments_in_create?
+ change_table_comment(table_name, comment) if comment
+
+ td.columns.each do |column|
+ change_column_comment(table_name, column.name, column.comment) if column.comment
+ end
+ end
+
result
end
@@ -776,7 +789,8 @@ module ActiveRecord
# [<tt>:type</tt>]
# The reference column type. Defaults to +:integer+.
# [<tt>:index</tt>]
- # Add an appropriate index. Defaults to false.
+ # Add an appropriate index. Defaults to false.
+ # See #add_index for usage of this option.
# [<tt>:foreign_key</tt>]
# Add an appropriate foreign key constraint. Defaults to false.
# [<tt>:polymorphic</tt>]
@@ -796,6 +810,14 @@ module ActiveRecord
#
# add_reference(:products, :supplier, polymorphic: true, index: true)
#
+ # ====== Create a supplier_id column with a unique index
+ #
+ # add_reference(:products, :supplier, index: { unique: true })
+ #
+ # ====== Create a supplier_id column with a named index
+ #
+ # add_reference(:products, :supplier, index: { name: "my_supplier_index" })
+ #
# ====== Create a supplier_id column and appropriate foreign key
#
# add_reference(:products, :supplier, foreign_key: true)
@@ -854,7 +876,7 @@ module ActiveRecord
#
# generates:
#
- # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
#
# ====== Creating a foreign key on a specific column
#
@@ -870,7 +892,7 @@ module ActiveRecord
#
# generates:
#
- # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_e74ce85cbc FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
#
# The +options+ hash can include the following keys:
# [<tt>:column</tt>]
@@ -1051,9 +1073,9 @@ module ActiveRecord
end
# Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+.
- # Additional options (like <tt>null: false</tt>) are forwarded to #add_column.
+ # Additional options (like +:null+) are forwarded to #add_column.
#
- # add_timestamps(:suppliers, null: false)
+ # add_timestamps(:suppliers, null: true)
#
def add_timestamps(table_name, options = {})
options[:null] = false if options[:null].nil?
@@ -1075,7 +1097,7 @@ module ActiveRecord
Table.new(table_name, base)
end
- def add_index_options(table_name, column_name, options = {}) #:nodoc:
+ def add_index_options(table_name, column_name, comment: nil, **options) #:nodoc:
column_names = Array(column_name)
options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
@@ -1106,13 +1128,23 @@ module ActiveRecord
end
index_columns = quoted_columns_for_index(column_names, options).join(", ")
- [index_name, index_type, index_columns, index_options, algorithm, using]
+ [index_name, index_type, index_columns, index_options, algorithm, using, comment]
end
def options_include_default?(options)
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
+ # Changes the comment for a table or removes it if +nil+.
+ def change_table_comment(table_name, comment)
+ raise NotImplementedError, "#{self.class} does not support changing table comments"
+ end
+
+ # Changes the comment for a column or removes it if +nil+.
+ def change_column_comment(table_name, column_name, comment) #:nodoc:
+ raise NotImplementedError, "#{self.class} does not support changing column comments"
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -1194,8 +1226,8 @@ module ActiveRecord
end
private
- def create_table_definition(name, temporary = false, options = nil, as = nil)
- TableDefinition.new(name, temporary, options, as)
+ def create_table_definition(*args)
+ TableDefinition.new(*args)
end
def create_alter_table(name)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 6ecdab6eb0..ca795cb1ad 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -188,7 +188,10 @@ module ActiveRecord
transaction = begin_transaction options
yield
rescue Exception => error
- rollback_transaction if transaction
+ if transaction
+ rollback_transaction
+ after_failure_actions(transaction, error)
+ end
raise
ensure
unless error
@@ -214,7 +217,16 @@ module ActiveRecord
end
private
+
NULL_TRANSACTION = NullTransaction.new
+
+ # Deallocate invalidated prepared statements outside of the transaction
+ def after_failure_actions(transaction, error)
+ return unless transaction.is_a?(RealTransaction)
+ return unless error.is_a?(ActiveRecord::PreparedStatementCacheExpired)
+ @connection.clear_cache!
+ end
+
end
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 d9b42d4283..c9716c9ca9 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,5 +1,4 @@
require 'active_record/type'
-require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/determine_if_preparable_visitor'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/sql_type_metadata'
@@ -28,7 +27,6 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
autoload :ConnectionHandler
- autoload :ConnectionManagement
end
autoload_under 'abstract' do
@@ -108,6 +106,7 @@ module ActiveRecord
@schema_cache = SchemaCache.new self
@visitor = nil
@prepared_statements = false
+ @quoted_column_names, @quoted_table_names = {}, {}
end
class Version
@@ -279,6 +278,16 @@ module ActiveRecord
false
end
+ # Does this adapter support metadata comments on database objects (tables, columns, indexes)?
+ def supports_comments?
+ false
+ end
+
+ # Can comments for tables, columns, and indexes be specified in create/alter table statements?
+ def supports_comments_in_create?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -398,7 +407,7 @@ module ActiveRecord
if can_perform_case_insensitive_comparison_for?(column)
table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new))
else
- case_sensitive_comparison(table, attribute, column, value)
+ table[attribute].eq(Arel::Nodes::BindParam.new)
end
end
@@ -422,8 +431,8 @@ module ActiveRecord
end
end
- def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
- Column.new(name, default, sql_type_metadata, null, default_function, collation)
+ def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc:
+ Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation)
end
def lookup_cast_type(sql_type) # :nodoc:
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 8751b6da4b..86ad422b06 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,6 +1,7 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/mysql/column'
require 'active_record/connection_adapters/mysql/explain_pretty_printer'
+require 'active_record/connection_adapters/mysql/quoting'
require 'active_record/connection_adapters/mysql/schema_creation'
require 'active_record/connection_adapters/mysql/schema_definitions'
require 'active_record/connection_adapters/mysql/schema_dumper'
@@ -11,6 +12,7 @@ require 'active_support/core_ext/string/strip'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
+ include MySQL::Quoting
include MySQL::ColumnDumper
include Savepoints
@@ -32,8 +34,6 @@ module ActiveRecord
class_attribute :emulate_booleans
self.emulate_booleans = true
- QUOTED_TRUE, QUOTED_FALSE = '1', '0'
-
NATIVE_DATABASE_TYPES = {
primary_key: "int auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
@@ -54,7 +54,6 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
- @quoted_column_names, @quoted_table_names = {}, {}
@visitor = Arel::Visitors::MySQL.new self
@@ -78,10 +77,14 @@ module ActiveRecord
}
end
- def version
+ def version #:nodoc:
@version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0])
end
+ def mariadb? # :nodoc:
+ full_version =~ /mariadb/i
+ end
+
# Returns true, since this connection adapter supports migrations.
def supports_migrations?
true
@@ -122,7 +125,11 @@ module ActiveRecord
end
def supports_datetime_with_precision?
- version >= '5.6.4'
+ if mariadb?
+ version >= '5.3.0'
+ else
+ version >= '5.6.4'
+ end
end
def supports_advisory_locks?
@@ -153,8 +160,8 @@ module ActiveRecord
raise NotImplementedError
end
- def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
- MySQL::Column.new(field, default, sql_type_metadata, null, default_function, collation)
+ def new_column(*args) #:nodoc:
+ MySQL::Column.new(*args)
end
# Must return the MySQL error number from the exception, if the exception has an
@@ -163,48 +170,6 @@ module ActiveRecord
raise NotImplementedError
end
- # QUOTING ==================================================
-
- def _quote(value) # :nodoc:
- if value.is_a?(Type::Binary::Data)
- "x'#{value.hex}'"
- else
- super
- end
- end
-
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
- end
-
- def quote_table_name(name) #:nodoc:
- @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
- end
-
- def quoted_true
- QUOTED_TRUE
- end
-
- def unquoted_true
- 1
- end
-
- def quoted_false
- QUOTED_FALSE
- end
-
- def unquoted_false
- 0
- end
-
- def quoted_date(value)
- if supports_datetime_with_precision?
- super
- else
- super.sub(/\.\d{6}\z/, '')
- end
- end
-
# REFERENTIAL INTEGRITY ====================================
def disable_referential_integrity #:nodoc:
@@ -301,9 +266,9 @@ module ActiveRecord
# create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
else
- execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
end
end
@@ -312,7 +277,7 @@ module ActiveRecord
# Example:
# drop_database('sebastian_development')
def drop_database(name) #:nodoc:
- execute "DROP DATABASE IF EXISTS `#{name}`"
+ execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}"
end
def current_database
@@ -408,7 +373,7 @@ module ActiveRecord
mysql_index_type = row[:Index_type].downcase.to_sym
index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil
index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil
- indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using)
+ indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using, row[:Index_comment].presence)
end
indexes.last.columns << row[:Column_name]
@@ -421,21 +386,28 @@ module ActiveRecord
# Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name) # :nodoc:
- sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
- execute_and_free(sql, 'SCHEMA') do |result|
- each_hash(result).map do |field|
- type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
- if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP"
- new_column(field[:Field], nil, type_metadata, field[:Null] == "YES", field[:Default], field[:Collation])
- else
- new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation])
- end
+ table_name = table_name.to_s
+ column_definitions(table_name).map do |field|
+ type_metadata = fetch_type_metadata(field[:Type], field[:Extra])
+ if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP"
+ default, default_function = nil, field[:Default]
+ else
+ default, default_function = field[:Default], nil
end
+ new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence)
end
end
- def create_table(table_name, options = {}) #:nodoc:
- super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
+ def table_comment(table_name) # :nodoc:
+ select_value(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT table_comment
+ FROM information_schema.tables
+ WHERE table_name=#{quote(table_name)}
+ SQL
+ end
+
+ def create_table(table_name, **options) #:nodoc:
+ super(table_name, options: 'ENGINE=InnoDB', **options)
end
def bulk_change_table(table_name, operations) #:nodoc:
@@ -518,8 +490,10 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
+ index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}"
+ sql << " COMMENT #{quote(comment)}" if comment
+ execute sql
end
def foreign_keys(table_name)
@@ -557,7 +531,12 @@ module ActiveRecord
raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip
# strip AUTO_INCREMENT
- raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
+ raw_table_options.sub!(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
+
+ # strip COMMENT
+ raw_table_options.sub!(/ COMMENT='.+'/, '')
+
+ raw_table_options
end
# Maps logical Rails types to MySQL-specific data types.
@@ -585,8 +564,7 @@ module ActiveRecord
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
- variables = select_all("select @@#{name} as 'Value'", 'SCHEMA')
- variables.first['Value'] unless variables.empty?
+ select_value("SELECT @@#{name}", 'SCHEMA')
rescue ActiveRecord::StatementInvalid
nil
end
@@ -608,20 +586,17 @@ module ActiveRecord
end
def case_sensitive_comparison(table, attribute, column, value)
- if value.nil? || column.case_sensitive?
- super
- else
+ if !value.nil? && column.collation && !column.case_sensitive?
table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new))
+ else
+ super
end
end
- def case_insensitive_comparison(table, attribute, column, value)
- if column.case_sensitive?
- super
- else
- table[attribute].eq(Arel::Nodes::BindParam.new)
- end
+ def can_perform_case_insensitive_comparison_for?(column)
+ column.case_sensitive?
end
+ private :can_perform_case_insensitive_comparison_for?
# In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use
# DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for
@@ -671,7 +646,7 @@ module ActiveRecord
register_integer_type m, %r(^smallint)i, limit: 2
register_integer_type m, %r(^tinyint)i, limit: 1
- m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
+ m.register_type %r(^tinyint\(1\))i, Type::Boolean.new if emulate_booleans
m.alias_type %r(year)i, 'integer'
m.alias_type %r(bit)i, 'binary'
@@ -741,6 +716,8 @@ module ActiveRecord
RecordNotUnique.new(message)
when 1452
InvalidForeignKey.new(message)
+ when 1406
+ ValueTooLong.new(message)
else
super
end
@@ -826,10 +803,6 @@ module ActiveRecord
subselect.from subsubselect.as('__active_record_temp')
end
- def mariadb?
- full_version =~ /mariadb/i
- end
-
def supports_rename_index?
mariadb? ? false : version >= '5.7.6'
end
@@ -850,9 +823,19 @@ module ActiveRecord
# Make MySQL reject illegal values rather than truncating or blanking them, see
# http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
- unless variables.has_key?('sql_mode') || defaults.include?(@config[:strict])
- variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
+ if sql_mode = variables.delete('sql_mode')
+ sql_mode = quote(sql_mode)
+ elsif !defaults.include?(strict_mode?)
+ if strict_mode?
+ sql_mode = "CONCAT(@@sql_mode, ',STRICT_ALL_TABLES')"
+ else
+ sql_mode = "REPLACE(@@sql_mode, 'STRICT_TRANS_TABLES', '')"
+ sql_mode = "REPLACE(#{sql_mode}, 'STRICT_ALL_TABLES', '')"
+ sql_mode = "REPLACE(#{sql_mode}, 'TRADITIONAL', '')"
+ end
+ sql_mode = "CONCAT(#{sql_mode}, ',NO_AUTO_VALUE_ON_ZERO')"
end
+ sql_mode_assignment = "@@SESSION.sql_mode = #{sql_mode}, " if sql_mode
# NAMES does not have an equals sign, see
# http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430
@@ -874,7 +857,13 @@ module ActiveRecord
end.compact.join(', ')
# ...and send them all in one query
- @connection.query "SET #{encoding} #{variable_assignments}"
+ @connection.query "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}"
+ end
+
+ def column_definitions(table_name) # :nodoc:
+ execute_and_free("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
+ each_hash(result)
+ end
end
def extract_foreign_key_action(structure, name, action) # :nodoc:
@@ -894,8 +883,8 @@ module ActiveRecord
create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- MySQL::TableDefinition.new(name, temporary, options, as)
+ def create_table_definition(*args) # :nodoc:
+ MySQL::TableDefinition.new(*args)
end
def integer_to_sql(limit) # :nodoc:
@@ -940,8 +929,8 @@ module ActiveRecord
class MysqlString < Type::String # :nodoc:
def serialize(value)
case value
- when true then "1"
- when false then "0"
+ when true then MySQL::Quoting::QUOTED_TRUE
+ when false then MySQL::Quoting::QUOTED_FALSE
else super
end
end
@@ -950,8 +939,8 @@ module ActiveRecord
def cast_value(value)
case value
- when true then "1"
- when false then "0"
+ when true then MySQL::Quoting::QUOTED_TRUE
+ when false then MySQL::Quoting::QUOTED_FALSE
else super
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 10f908538f..ce2f9f1925 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module ConnectionAdapters
# An abstract definition of a column in a table.
class Column
- attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation
+ attr_reader :name, :default, :sql_type_metadata, :null, :table_name, :default_function, :collation, :comment
delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true
@@ -15,14 +15,15 @@ module ActiveRecord
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil)
+ def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil)
@name = name.freeze
+ @table_name = table_name
@sql_type_metadata = sql_type_metadata
@null = null
@default = default
@default_function = default_function
@collation = collation
- @table_name = nil
+ @comment = comment
end
def has_default?
@@ -54,7 +55,7 @@ module ActiveRecord
protected
def attributes_for_hash
- [self.class, name, default, sql_type_metadata, null, default_function, collation]
+ [self.class, name, default, sql_type_metadata, null, table_name, default_function, collation]
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
new file mode 100644
index 0000000000..fbab654112
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb
@@ -0,0 +1,51 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module MySQL
+ module Quoting # :nodoc:
+ QUOTED_TRUE, QUOTED_FALSE = '1', '0'
+
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`"
+ end
+
+ def quote_table_name(name)
+ @quoted_table_names[name] ||= super.gsub('.', '`.`')
+ end
+
+ def quoted_true
+ QUOTED_TRUE
+ end
+
+ def unquoted_true
+ 1
+ end
+
+ def quoted_false
+ QUOTED_FALSE
+ end
+
+ def unquoted_false
+ 0
+ end
+
+ def quoted_date(value)
+ if supports_datetime_with_precision?
+ super
+ else
+ super.sub(/\.\d{6}\z/, '')
+ end
+ end
+
+ private
+
+ def _quote(value)
+ if value.is_a?(Type::Binary::Data)
+ "x'#{value.hex}'"
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
index 1e2c859af9..0384079da2 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -2,6 +2,9 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
class SchemaCreation < AbstractAdapter::SchemaCreation
+ delegate :quote, to: :@conn
+ private :quote
+
private
def visit_DropForeignKey(name)
@@ -22,6 +25,14 @@ module ActiveRecord
add_column_position!(change_column_sql, column_options(o.column))
end
+ def add_table_options!(create_sql, options)
+ super
+
+ if comment = options[:comment]
+ create_sql << " COMMENT #{quote(comment)}"
+ end
+ end
+
def column_options(o)
column_options = super
column_options[:charset] = o.charset
@@ -29,13 +40,21 @@ module ActiveRecord
end
def add_column_options!(sql, options)
- if options[:charset]
- sql << " CHARACTER SET #{options[:charset]}"
+ if charset = options[:charset]
+ sql << " CHARACTER SET #{charset}"
end
- if options[:collation]
- sql << " COLLATE #{options[:collation]}"
+
+ if collation = options[:collation]
+ sql << " COLLATE #{collation}"
end
+
super
+
+ if comment = options[:comment]
+ sql << " COMMENT #{quote(comment)}"
+ end
+
+ sql
end
def add_column_position!(sql, options)
@@ -44,12 +63,14 @@ module ActiveRecord
elsif options[:after]
sql << " AFTER #{quote_column_name(options[:after])}"
end
+
sql
end
def index_in_create(table_name, column_name, options)
- index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options)
- "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) "
+ index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options)
+ index_option = " COMMENT #{quote(comment)}" if comment
+ "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_option} "
end
end
end
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 ccf5b6cadc..2ba9657f24 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -3,18 +3,13 @@ module ActiveRecord
module MySQL
module ColumnDumper
def column_spec_for_primary_key(column)
- spec = {}
if column.bigint?
- spec[:id] = ':bigint'
+ spec = { id: :bigint.inspect }
spec[:default] = schema_default(column) || 'nil' unless column.auto_increment?
- spec[:unsigned] = 'true' if column.unsigned?
- elsif column.auto_increment?
- spec[:unsigned] = 'true' if column.unsigned?
- return if spec.empty?
else
- spec[:id] = schema_type(column).inspect
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
+ spec = super
end
+ spec[:unsigned] = 'true' if column.unsigned?
spec
end
@@ -30,6 +25,10 @@ module ActiveRecord
private
+ def default_primary_key?(column)
+ super && column.auto_increment?
+ end
+
def schema_type(column)
if column.sql_type == 'tinyblob'
:blob
@@ -38,16 +37,12 @@ module ActiveRecord
end
end
- def schema_limit(column)
- super unless column.type == :boolean
- end
-
def schema_precision(column)
super unless /time/ === column.sql_type && column.precision == 0
end
def schema_collation(column)
- if column.collation && table_name = column.instance_variable_get(:@table_name)
+ if column.collation && table_name = column.table_name
@table_collation_cache ||= {}
@table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"]
column.collation.inspect if column.collation != @table_collation_cache[table_name]
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 23b33a3555..ec343a5a57 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -16,7 +16,7 @@ module ActiveRecord
if config[:flags].kind_of? Array
config[:flags].push "FOUND_ROWS".freeze
else
- config[:flags] |= Mysql2::Client::FOUND_ROWS
+ config[:flags] |= Mysql2::Client::FOUND_ROWS
end
end
@@ -45,6 +45,14 @@ module ActiveRecord
!mariadb? && version >= '5.7.8'
end
+ def supports_comments?
+ true
+ end
+
+ def supports_comments_in_create?
+ true
+ end
+
# HELPER METHODS ===========================================
def each_hash(result) # :nodoc:
@@ -131,13 +139,7 @@ module ActiveRecord
def exec_query(sql, name = 'SQL', binds = [], prepare: false)
result = execute(sql, name)
@connection.next_result while @connection.more_results?
- ActiveRecord::Result.new(result.fields, result.to_a)
- end
-
- alias exec_without_stmt exec_query
-
- def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
- execute to_sql(sql, binds), name
+ ActiveRecord::Result.new(result.fields, result.to_a) if result
end
def exec_delete(sql, name, binds)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index bfa03fa136..3ad1911a28 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -8,7 +8,6 @@ module ActiveRecord
def serial?
return unless default_function
- table_name = @table_name || '(?<table_name>.+)'
%r{\Anextval\('"?#{table_name}_#{name}_seq"?'::regclass\)\z} === default_function
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 6aa264d766..6f2e03b370 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -118,7 +118,7 @@ module ActiveRecord
alias :exec_update :exec_delete
def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc:
- unless pk
+ if pk.nil?
# Extract the table from the insert sql. Yuck.
table_ref = extract_table_ref_from_insert_sql(sql)
pk = primary_key(table_ref) if table_ref
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index c1c77a967e..6414459cd1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -27,8 +27,8 @@ module ActiveRecord
# - schema_name."table.name"
# - "schema.name".table_name
# - "schema.name"."table.name"
- def quote_table_name(name)
- Utils.extract_schema_qualified_name(name.to_s).quoted
+ def quote_table_name(name) # :nodoc:
+ @quoted_table_names[name] ||= Utils.extract_schema_qualified_name(name.to_s).quoted
end
# Quotes schema names for use in SQL queries.
@@ -41,8 +41,8 @@ module ActiveRecord
end
# Quotes column names for use in SQL queries.
- def quote_column_name(name) #:nodoc:
- PGconn.quote_ident(name.to_s)
+ def quote_column_name(name) # :nodoc:
+ @quoted_column_names[name] ||= PGconn.quote_ident(super)
end
# Quote date/time values for use in SQL input.
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
index b82bdb8b0c..a1e10fd364 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -3,16 +3,9 @@ module ActiveRecord
module PostgreSQL
module ColumnDumper
def column_spec_for_primary_key(column)
- spec = {}
- if column.serial?
- return unless column.bigint?
- spec[:id] = ':bigserial'
- elsif column.type == :uuid
- spec[:id] = ':uuid'
- spec[:default] = schema_default(column) || 'nil'
- else
- spec[:id] = schema_type(column).inspect
- spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) })
+ spec = super
+ if schema_type(column) == :uuid
+ spec[:default] ||= 'nil'
end
spec
end
@@ -31,6 +24,10 @@ module ActiveRecord
private
+ def default_primary_key?(column)
+ schema_type(column) == :serial
+ end
+
def schema_type(column)
return super unless column.serial?
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 67e727d8ed..4a66b82cbb 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/strip'
+
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -172,7 +174,8 @@ module ActiveRecord
table = Utils.extract_schema_qualified_name(table_name.to_s)
result = query(<<-SQL, 'SCHEMA')
- SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid
+ SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
@@ -190,6 +193,7 @@ module ActiveRecord
indkey = row[2].split(" ").map(&:to_i)
inddef = row[3]
oid = row[4]
+ comment = row[5]
columns = Hash[query(<<-SQL, "SCHEMA")]
SELECT a.attnum, a.attname
@@ -207,26 +211,41 @@ module ActiveRecord
where = inddef.scan(/WHERE (.+)$/).flatten[0]
using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
- IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using, comment)
end
end.compact
end
# Returns the list of all column definitions for a table.
- def columns(table_name)
- # Limit, precision, and scale are all handled by the superclass.
- column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation|
+ def columns(table_name) # :nodoc:
+ table_name = table_name.to_s
+ column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment|
oid = oid.to_i
fmod = fmod.to_i
type_metadata = fetch_type_metadata(column_name, type, oid, fmod)
default_value = extract_value_from_default(default)
default_function = extract_default_function(default_value, default)
- new_column(column_name, default_value, type_metadata, !notnull, default_function, collation)
+ new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation, comment: comment)
end
end
- def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc:
- PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation)
+ def new_column(*args) # :nodoc:
+ PostgreSQLColumn.new(*args)
+ end
+
+ # Returns a comment stored in database for given table
+ def table_comment(table_name) # :nodoc:
+ name = Utils.extract_schema_qualified_name(table_name.to_s)
+ if name.identifier
+ select_value(<<-SQL.strip_heredoc, 'SCHEMA')
+ SELECT pg_catalog.obj_description(c.oid, 'pg_class')
+ FROM pg_catalog.pg_class c
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
+ WHERE c.relname = #{quote(name.identifier)}
+ AND c.relkind IN ('r') -- (r)elation/table
+ AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'}
+ SQL
+ end
end
# Returns the current database name.
@@ -325,7 +344,7 @@ module ActiveRecord
select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA')
else
- @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
+ @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger
end
end
end
@@ -340,7 +359,7 @@ module ActiveRecord
end
if @logger && pk && !sequence
- @logger.warn "#{table} has primary key #{pk} with no default sequence"
+ @logger.warn "#{table} has primary key #{pk} with no default sequence."
end
if pk && sequence
@@ -445,6 +464,7 @@ module ActiveRecord
def add_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
super
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
@@ -466,6 +486,7 @@ module ActiveRecord
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
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)
end
# Changes the default value of a table column.
@@ -494,6 +515,18 @@ module ActiveRecord
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
+ # Adds comment for given table column or drops it if +comment+ is a +nil+
+ def change_column_comment(table_name, column_name, comment) # :nodoc:
+ clear_cache!
+ execute "COMMENT ON COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} IS #{quote(comment)}"
+ end
+
+ # Adds comment for given table or drops it if +comment+ is a +nil+
+ def change_table_comment(table_name, comment) # :nodoc:
+ clear_cache!
+ execute "COMMENT ON TABLE #{quote_table_name(table_name)} IS #{quote(comment)}"
+ end
+
# Renames a column in a table.
def rename_column(table_name, column_name, new_column_name) #:nodoc:
clear_cache!
@@ -502,8 +535,10 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
- execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do
+ execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment
+ end
end
def remove_index(table_name, options = {}) #:nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index beaeef3c78..d7f9378a4e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -159,6 +159,14 @@ module ActiveRecord
postgresql_version >= 90200
end
+ def supports_comments?
+ true
+ end
+
+ def supports_comments_in_create?
+ false
+ end
+
def index_algorithms
{ concurrently: 'CONCURRENTLY' }
end
@@ -398,6 +406,7 @@ module ActiveRecord
protected
# See http://www.postgresql.org/docs/current/static/errcodes-appendix.html
+ VALUE_LIMIT_VIOLATION = "22001"
FOREIGN_KEY_VIOLATION = "23503"
UNIQUE_VIOLATION = "23505"
@@ -409,6 +418,8 @@ module ActiveRecord
RecordNotUnique.new(message)
when FOREIGN_KEY_VIOLATION
InvalidForeignKey.new(message)
+ when VALUE_LIMIT_VIOLATION
+ ValueTooLong.new(message)
else
super
end
@@ -598,25 +609,41 @@ module ActiveRecord
@connection.exec_prepared(stmt_key, type_casted_binds)
end
rescue ActiveRecord::StatementInvalid => e
- pgerror = e.cause
+ raise unless is_cached_plan_failure?(e)
- # Get the PG code for the failure. Annoyingly, the code for
- # prepared statements whose return value may have changed is
- # FEATURE_NOT_SUPPORTED. Check here for more details:
- # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
- begin
- code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
- rescue
- raise e
- end
- if FEATURE_NOT_SUPPORTED == code
+ # Nothing we can do if we are in a transaction because all commands
+ # will raise InFailedSQLTransaction
+ if in_transaction?
+ raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message)
+ else
+ # outside of transactions we can simply flush this query and retry
@statements.delete sql_key(sql)
retry
- else
- raise e
end
end
+ # Annoyingly, the code for prepared statements whose return value may
+ # have changed is FEATURE_NOT_SUPPORTED.
+ #
+ # This covers various different error types so we need to do additional
+ # work to classify the exception definitively as a
+ # ActiveRecord::PreparedStatementCacheExpired
+ #
+ # Check here for more details:
+ # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
+ CACHED_PLAN_HEURISTIC = 'cached plan must not change result type'.freeze
+ def is_cached_plan_failure?(e)
+ pgerror = e.cause
+ code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC)
+ rescue
+ false
+ end
+
+ def in_transaction?
+ open_transactions > 0
+ end
+
# Returns the statement identifier for the client side cache
# of statements
def sql_key(sql)
@@ -696,7 +723,7 @@ module ActiveRecord
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
- # SELECT column.name, column.type, default.value
+ # SELECT column.name, column.type, default.value, column.comment
# FROM column LEFT JOIN default
# ON column.table_id = default.table_id
# AND column.num = default.column_num
@@ -716,7 +743,8 @@ module ActiveRecord
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod,
(SELECT c.collname FROM pg_collation c, pg_type t
- WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation)
+ WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation),
+ col_description(a.attrelid, a.attnum) AS comment
FROM pg_attribute a LEFT JOIN pg_attrdef d
ON a.attrelid = d.adrelid AND a.attnum = d.adnum
WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass
@@ -730,8 +758,8 @@ module ActiveRecord
$1.strip if $1
end
- def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
- PostgreSQL::TableDefinition.new(name, temporary, options, as)
+ def create_table_definition(*args) # :nodoc:
+ PostgreSQL::TableDefinition.new(*args)
end
def can_perform_case_insensitive_comparison_for?(column)
@@ -799,7 +827,7 @@ module ActiveRecord
ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql)
ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql)
ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql)
- ActiveRecord::Type.register(:date_time, OID::DateTime, adapter: :postgresql)
+ ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql)
ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql)
ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
new file mode 100644
index 0000000000..d5a181d3e2
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -0,0 +1,48 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module SQLite3
+ module Quoting # :nodoc:
+ def quote_string(s)
+ @connection.class.quote(s)
+ end
+
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
+ end
+
+ def quote_column_name(name)
+ @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}")
+ end
+
+ def quoted_time(value)
+ quoted_date(value)
+ end
+
+ private
+
+ def _quote(value)
+ if value.is_a?(Type::Binary::Data)
+ "x'#{value.hex}'"
+ else
+ super
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when BigDecimal
+ value.to_f
+ when String
+ if value.encoding == Encoding::ASCII_8BIT
+ super(value.encode(Encoding::UTF_8))
+ else
+ super
+ end
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
index fe1dcbd710..70c0d28830 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb
@@ -3,6 +3,13 @@ module ActiveRecord
module SQLite3
class SchemaCreation < AbstractAdapter::SchemaCreation
private
+
+ def column_options(o)
+ options = super
+ options[:null] = false if o.primary_key
+ options
+ end
+
def add_column_options!(sql, options)
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index c65d33ccb3..786b0ab2ed 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,6 +1,7 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
require 'active_record/connection_adapters/sqlite3/explain_pretty_printer'
+require 'active_record/connection_adapters/sqlite3/quoting'
require 'active_record/connection_adapters/sqlite3/schema_creation'
gem 'sqlite3', '~> 1.3.6'
@@ -49,6 +50,8 @@ module ActiveRecord
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
ADAPTER_NAME = 'SQLite'.freeze
+
+ include SQLite3::Quoting
include Savepoints
NATIVE_DATABASE_TYPES = {
@@ -84,7 +87,6 @@ module ActiveRecord
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@visitor = Arel::Visitors::SQLite.new self
- @quoted_column_names = {}
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
@@ -174,44 +176,6 @@ module ActiveRecord
true
end
- # QUOTING ==================================================
-
- def _quote(value) # :nodoc:
- case value
- when Type::Binary::Data
- "x'#{value.hex}'"
- else
- super
- end
- end
-
- def _type_cast(value) # :nodoc:
- case value
- when BigDecimal
- value.to_f
- when String
- if value.encoding == Encoding::ASCII_8BIT
- super(value.encode(Encoding::UTF_8))
- else
- super
- end
- else
- super
- end
- end
-
- def quote_string(s) #:nodoc:
- @connection.class.quote(s)
- end
-
- def quote_table_name_for_assignment(table, attr)
- quote_column_name(attr)
- end
-
- def quote_column_name(name) #:nodoc:
- @quoted_column_names[name] ||= %Q("#{name.to_s.gsub('"', '""')}")
- end
-
#--
# DATABASE STATEMENTS ======================================
#++
@@ -337,7 +301,8 @@ module ActiveRecord
end
# Returns an array of +Column+ objects for the table specified by +table_name+.
- def columns(table_name) #:nodoc:
+ def columns(table_name) # :nodoc:
+ table_name = table_name.to_s
table_structure(table_name).map do |field|
case field["dflt_value"]
when /^null$/i
@@ -351,7 +316,7 @@ module ActiveRecord
collation = field['collation']
sql_type = field['type']
type_metadata = fetch_type_metadata(sql_type)
- new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, nil, collation)
+ new_column(field['name'], field['dflt_value'], type_metadata, field['notnull'].to_i == 0, table_name, nil, collation)
end
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 24fd0aaecf..5d74631e32 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -72,6 +72,14 @@ module ActiveRecord
##
# :singleton-method:
+ # Specifies if an error should be raised on query limit or order being
+ # ignored when doing batch queries. Useful in applications where the
+ # limit or scope being ignored is error-worthy, rather than a warning.
+ mattr_accessor :error_on_ignored_order_or_limit, instance_writer: false
+ self.error_on_ignored_order_or_limit = false
+
+ ##
+ # :singleton-method:
# Specify whether or not to use timestamps for migration versions
mattr_accessor :timestamped_migrations, instance_writer: false
self.timestamped_migrations = true
@@ -128,7 +136,7 @@ module ActiveRecord
end
def initialize_find_by_cache # :nodoc:
- @find_by_statement_cache = {}.extend(Mutex_m)
+ @find_by_statement_cache = { true => {}.extend(Mutex_m), false => {}.extend(Mutex_m) }
end
def inherited(child_class) # :nodoc:
@@ -151,7 +159,7 @@ module ActiveRecord
id = id.id
ActiveSupport::Deprecation.warn(<<-MSG.squish)
You are passing an instance of ActiveRecord::Base to `find`.
- Please pass the id of the object by calling `.id`
+ Please pass the id of the object by calling `.id`.
MSG
end
@@ -272,8 +280,9 @@ module ActiveRecord
private
def cached_find_by_statement(key, &block) # :nodoc:
- @find_by_statement_cache[key] || @find_by_statement_cache.synchronize {
- @find_by_statement_cache[key] ||= StatementCache.create(connection, &block)
+ cache = @find_by_statement_cache[connection.prepared_statements]
+ cache[key] || cache.synchronize {
+ cache[key] ||= StatementCache.create(connection, &block)
}
end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index 903c63a7db..7be332fb97 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -152,7 +152,7 @@ module ActiveRecord
enum_values = ActiveSupport::HashWithIndifferentAccess.new
name = name.to_sym
- # def self.statuses statuses end
+ # def self.statuses() statuses end
detect_enum_conflict!(name, name.to_s.pluralize, true)
klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 87f32c042c..b8b8684cff 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -125,6 +125,10 @@ module ActiveRecord
class InvalidForeignKey < WrappedDatabaseException
end
+ # Raised when a record cannot be inserted or updated because a value too long for a column type.
+ class ValueTooLong < StatementInvalid
+ end
+
# Raised when number of bind variables in statement given to +:condition+ key
# (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method)
# does not match number of expected values supplied.
@@ -139,6 +143,11 @@ module ActiveRecord
class NoDatabaseError < StatementInvalid
end
+ # Raised when Postgres returns 'cached plan must not change result type' and
+ # we cannot retry gracefully (e.g. inside a transaction)
+ class PreparedStatementCacheExpired < StatementInvalid
+ end
+
# Raised on attempt to save stale record. Record is stale when it's being saved in another query after
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index f969556c50..e4a44244e2 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -52,9 +52,15 @@ module ActiveRecord
end
end
+ def prepare_erb(content)
+ erb = ERB.new(content)
+ erb.filename = @file
+ erb
+ end
+
def render(content)
context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
- ERB.new(content).result(context.get_binding)
+ prepare_erb(content).result(context.get_binding)
end
# Validate our unmarshalled data.
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index aa1f5c4fb4..73be4cb271 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -8,7 +8,7 @@ module ActiveRecord
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index b63caa4473..efa2a4df02 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -67,7 +67,7 @@ module ActiveRecord
case sql
when /\A\s*rollback/mi
RED
- when /\s*.*?select .*for update/mi, /\A\s*lock/mi
+ when /select .*for update/mi, /\A\s*lock/mi
WHITE
when /\A\s*select/i
BLUE
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 4419a7b1e7..99a79024ad 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -156,8 +156,8 @@ module ActiveRecord
class ProtectedEnvironmentError < ActiveRecordError #:nodoc:
def initialize(env = "production")
- msg = "You are attempting to run a destructive action against your '#{env}' database\n"
- msg << "If you are sure you want to continue, run the same command with the environment variable\n"
+ msg = "You are attempting to run a destructive action against your '#{env}' database.\n"
+ msg << "If you are sure you want to continue, run the same command with the environment variable:\n"
msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1"
super(msg)
end
@@ -528,7 +528,7 @@ module ActiveRecord
name = "V#{version.tr('.', '_')}"
unless Compatibility.const_defined?(name)
versions = Compatibility.constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete('V').tr('_', '.').inspect }
- raise "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}"
+ raise ArgumentError, "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}"
end
Compatibility.const_get(name)
end
@@ -540,7 +540,7 @@ module ActiveRecord
MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc:
# This class is used to verify that all migrations have been run before
- # loading a web page if config.active_record.migration_error is set to :page_load
+ # loading a web page if <tt>config.active_record.migration_error</tt> is set to :page_load
class CheckPending
def initialize(app)
@app = app
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 45e35a4f71..a20d7e0820 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -57,7 +57,7 @@ module ActiveRecord
def index_exists?(table_name, column_name, options = {})
column_names = Array(column_name).map(&:to_s)
options[:name] =
- if options.key?(:name).present?
+ if options[:name].present?
options[:name].to_s
else
index_name(table_name, column: column_names)
@@ -102,7 +102,7 @@ module ActiveRecord
module Legacy
include FourTwoShared
- def run(*)
+ def migrate(*)
ActiveSupport::Deprecation.warn \
"Directly inheriting from ActiveRecord::Migration is deprecated. " \
"Please specify the Rails release the migration was written for:\n" \
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index ee52c3ae02..52eab952e1 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -231,6 +231,18 @@ module ActiveRecord
@explicit_sequence_name = true
end
+ # Determines if the primary key values should be selected from their
+ # corresponding sequence before the insert statement.
+ def prefetch_primary_key?
+ connection.prefetch_primary_key?(table_name)
+ end
+
+ # Returns the next value that will be used as the primary key on
+ # an insert statment.
+ def next_sequence_value
+ connection.next_sequence_value(sequence_name)
+ end
+
# Indicates whether the table associated with this class exists
def table_exists?
connection.schema_cache.data_source_exists?(table_name)
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 0d5a8e6f25..fe68869143 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -195,19 +195,27 @@ module ActiveRecord
# Nested attributes for an associated collection can also be passed in
# the form of a hash of hashes instead of an array of hashes:
#
- # Member.create(name: 'joe',
- # posts_attributes: { first: { title: 'Foo' },
- # second: { title: 'Bar' } })
+ # Member.create(
+ # name: 'joe',
+ # posts_attributes: {
+ # first: { title: 'Foo' },
+ # second: { title: 'Bar' }
+ # }
+ # )
#
# has the same effect as
#
- # Member.create(name: 'joe',
- # posts_attributes: [ { title: 'Foo' },
- # { title: 'Bar' } ])
+ # Member.create(
+ # name: 'joe',
+ # posts_attributes: [
+ # { title: 'Foo' },
+ # { title: 'Bar' }
+ # ]
+ # )
#
# The keys of the hash which is the value for +:posts_attributes+ are
# ignored in this case.
- # However, it is not allowed to use +'id'+ or +:id+ for one of
+ # However, it is not allowed to use <tt>'id'</tt> or <tt>:id</tt> for one of
# such keys, otherwise the hash will be wrapped in an array and
# interpreted as an attribute hash for a single post.
#
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 0b500346bc..1ab4e0404f 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module NullRelation # :nodoc:
def exec_queries
- @records = []
+ @records = [].freeze
end
def pluck(*column_names)
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index d9a394fb71..afed5e5e85 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -61,7 +61,7 @@ module ActiveRecord
# +instantiate+ instead of +new+, finder methods ensure they get new
# instances of the appropriate class for each record.
#
- # See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see
+ # See <tt>ActiveRecord::Inheritance#discriminate_class_for_record</tt> to see
# how this "single-table" inheritance mapping is implemented.
def instantiate(attributes, column_types = {})
klass = discriminate_class_for_record(attributes)
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index dcb2bd3d84..bbbc824b25 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -23,34 +23,30 @@ module ActiveRecord
end
end
- def initialize(app)
- @app = app
- end
-
- def call(env)
+ def self.run
connection = ActiveRecord::Base.connection
enabled = connection.query_cache_enabled
connection_id = ActiveRecord::Base.connection_id
connection.enable_query_cache!
- response = @app.call(env)
- response[2] = Rack::BodyProxy.new(response[2]) do
- restore_query_cache_settings(connection_id, enabled)
- end
-
- response
- rescue Exception => e
- restore_query_cache_settings(connection_id, enabled)
- raise e
+ [enabled, connection_id]
end
- private
+ def self.complete(state)
+ enabled, connection_id = state
- def restore_query_cache_settings(connection_id, enabled)
ActiveRecord::Base.connection_id = connection_id
ActiveRecord::Base.connection.clear_query_cache
ActiveRecord::Base.connection.disable_query_cache! unless enabled
end
+ def self.install_executor_hooks(executor = ActiveSupport::Executor)
+ executor.register_hook(self)
+
+ executor.to_complete do
+ # FIXME: This should be skipped when env['rack.test']
+ ActiveRecord::Base.clear_active_connections!
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index de5b42e987..4e32d73001 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,6 +1,6 @@
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
+ delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :empty?, :none?, :one?, to: :all
delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index f4200e96b7..98ea425d16 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -16,12 +16,6 @@ module ActiveRecord
config.app_generators.orm :active_record, :migration => true,
:timestamps => true
- config.app_middleware.insert_after ::ActionDispatch::Callbacks,
- ActiveRecord::QueryCache
-
- config.app_middleware.insert_after ::ActionDispatch::Callbacks,
- ActiveRecord::ConnectionAdapters::ConnectionManagement
-
config.action_dispatch.rescue_responses.merge!(
'ActiveRecord::RecordNotFound' => :not_found,
'ActiveRecord::StaleObjectError' => :conflict,
@@ -71,7 +65,6 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) do
self.time_zone_aware_attributes = true
self.default_timezone = :utc
- self.time_zone_aware_types = ActiveRecord::Base.time_zone_aware_types
end
end
@@ -153,11 +146,9 @@ end_warning
end
end
- initializer "active_record.set_reloader_hooks" do |app|
- hook = app.config.reload_classes_only_on_change ? :to_prepare : :to_cleanup
-
+ initializer "active_record.set_reloader_hooks" do
ActiveSupport.on_load(:active_record) do
- ActionDispatch::Reloader.send(hook) do
+ ActiveSupport::Reloader.before_class_unload do
if ActiveRecord::Base.connected?
ActiveRecord::Base.clear_cache!
ActiveRecord::Base.clear_reloadable_connections!
@@ -166,6 +157,12 @@ end_warning
end
end
+ initializer "active_record.set_executor_hooks" do
+ ActiveSupport.on_load(:active_record) do
+ ActiveRecord::QueryCache.install_executor_hooks
+ end
+ end
+
initializer "active_record.add_watchable_files" do |app|
path = app.paths["db"].first
config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"]
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 69a7838001..fc1b62ee96 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -22,7 +22,7 @@ db_namespace = namespace :db do
end
end
- desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV, it defaults to creating the development and test databases.'
+ desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases.'
task :create => [:load_config] do
ActiveRecord::Tasks::DatabaseTasks.create_current
end
@@ -33,7 +33,7 @@ db_namespace = namespace :db do
end
end
- desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.'
+ desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases.'
task :drop => [:load_config, :check_protected_environments] do
db_namespace["drop:_unsafe"].invoke
end
@@ -256,7 +256,7 @@ db_namespace = namespace :db do
end
desc 'Loads a schema.rb file into the database'
- task :load => [:environment, :load_config] do
+ task :load => [:environment, :load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA'])
end
@@ -278,7 +278,7 @@ db_namespace = namespace :db do
desc 'Clears a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
- FileUtils.rm(filename) if File.exist?(filename)
+ rm_f filename, verbose: false
end
end
@@ -302,7 +302,7 @@ db_namespace = namespace :db do
end
desc "Recreates the databases from the structure.sql file"
- task :load => [:environment, :load_config] do
+ task :load => [:environment, :load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA'])
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 956fe7c51e..bf398b0d40 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -138,6 +138,10 @@ module ActiveRecord
# PolymorphicReflection
# RuntimeReflection
class AbstractReflection # :nodoc:
+ def through_reflection?
+ false
+ end
+
def table_name
klass.table_name
end
@@ -445,6 +449,10 @@ module ActiveRecord
scope ? [[scope]] : [[]]
end
+ def has_scope?
+ scope
+ end
+
def has_inverse?
inverse_name
end
@@ -700,6 +708,10 @@ module ActiveRecord
@source_reflection_name = delegate_reflection.options[:source]
end
+ def through_reflection?
+ true
+ end
+
def klass
@klass ||= delegate_reflection.compute_class(class_name)
end
@@ -765,7 +777,6 @@ module ActiveRecord
# This is for clearing cache on the reflection. Useful for tests that need to compare
# SQL queries on associations.
def clear_association_scope_cache # :nodoc:
- @chain = nil
delegate_reflection.clear_association_scope_cache
source_reflection.clear_association_scope_cache
through_reflection.clear_association_scope_cache
@@ -812,13 +823,19 @@ module ActiveRecord
end
end
+ def has_scope?
+ scope || options[:source_type] ||
+ source_reflection.has_scope? ||
+ through_reflection.has_scope?
+ end
+
def join_keys(association_klass)
source_reflection.join_keys(association_klass)
end
# A through association is nested if there would be more than one join table
def nested?
- chain.length > 2
+ source_reflection.through_reflection? || through_reflection.through_reflection?
end
# We want to use the klass from this reflection, rather than just delegate straight to
@@ -857,7 +874,7 @@ module ActiveRecord
example_options = options.dup
example_options[:source] = source_reflection_names.first
ActiveSupport::Deprecation.warn \
- "Ambiguous source reflection for through association. Please " \
+ "Ambiguous source reflection for through association. Please " \
"specify a :source directive on your declaration like:\n" \
"\n" \
" class #{active_record.name} < ActiveRecord::Base\n" \
@@ -995,7 +1012,7 @@ module ActiveRecord
end
def constraints
- [source_type_info]
+ @reflection.constraints + [source_type_info]
end
def source_type_info
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 7e842668c6..d042fe5f8b 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -45,8 +45,8 @@ module ActiveRecord
k.name == primary_key
}]
- if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
- primary_key_value = connection.next_sequence_value(klass.sequence_name)
+ if !primary_key_value && klass.prefetch_primary_key?
+ primary_key_value = klass.next_sequence_value
values[arel_attribute(klass.primary_key)] = primary_key_value
end
end
@@ -94,12 +94,12 @@ module ActiveRecord
end
def substitute_values(values) # :nodoc:
- binds = values.map do |arel_attr, value|
- QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name))
- end
+ binds = []
+ substitutes = []
- substitutes = values.map do |(arel_attr, _)|
- [arel_attr, Arel::Nodes::BindParam.new]
+ values.each do |arel_attr, value|
+ binds.push QueryAttribute.new(arel_attr.name, value, klass.type_for_attribute(arel_attr.name))
+ substitutes.push [arel_attr, Arel::Nodes::BindParam.new]
end
[substitutes, binds]
@@ -253,17 +253,21 @@ module ActiveRecord
# Converts relation objects to Array.
def to_a
+ records.dup
+ end
+
+ def records # :nodoc:
load
@records
end
# Serializes the relation objects Array.
def encode_with(coder)
- coder.represent_seq(nil, to_a)
+ coder.represent_seq(nil, records)
end
def as_json(options = nil) #:nodoc:
- to_a.as_json(options)
+ records.as_json(options)
end
# Returns size of the records.
@@ -298,13 +302,13 @@ module ActiveRecord
# Returns true if there is exactly one record.
def one?
return super if block_given?
- limit_value ? to_a.one? : size == 1
+ limit_value ? records.one? : size == 1
end
# Returns true if there is more than one record.
def many?
return super if block_given?
- limit_value ? to_a.many? : size > 1
+ limit_value ? records.many? : size > 1
end
# Returns a cache key that can be used to identify the records fetched by
@@ -418,13 +422,13 @@ module ActiveRecord
if id.is_a?(Array)
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
elsif id == :all
- to_a.each { |record| record.update(attributes) }
+ records.each { |record| record.update(attributes) }
else
if ActiveRecord::Base === id
id = id.id
ActiveSupport::Deprecation.warn(<<-MSG.squish)
You are passing an instance of ActiveRecord::Base to `update`.
- Please pass the id of the object by calling `.id`
+ Please pass the id of the object by calling `.id`.
MSG
end
object = find(id)
@@ -453,11 +457,11 @@ module ActiveRecord
if conditions
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1.
- To achieve the same use where(conditions).destroy_all
+ To achieve the same use where(conditions).destroy_all.
MESSAGE
where(conditions).destroy_all
else
- to_a.each(&:destroy).tap { reset }
+ records.each(&:destroy).tap { reset }
end
end
@@ -523,7 +527,7 @@ module ActiveRecord
if conditions
ActiveSupport::Deprecation.warn(<<-MESSAGE.squish)
Passing conditions to delete_all is deprecated and will be removed in Rails 5.1.
- To achieve the same use where(conditions).delete_all
+ To achieve the same use where(conditions).delete_all.
MESSAGE
where(conditions).delete_all
else
@@ -587,7 +591,7 @@ module ActiveRecord
def reset
@last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
@should_eager_load = @join_dependency = nil
- @records = []
+ @records = [].freeze
@offsets = {}
self
end
@@ -654,21 +658,21 @@ module ActiveRecord
def ==(other)
case other
when Associations::CollectionProxy, AssociationRelation
- self == other.to_a
+ self == other.records
when Relation
other.to_sql == to_sql
when Array
- to_a == other
+ records == other
end
end
def pretty_print(q)
- q.pp(self.to_a)
+ q.pp(self.records)
end
# Returns true if relation is blank.
def blank?
- to_a.blank?
+ records.blank?
end
def values
@@ -676,7 +680,7 @@ module ActiveRecord
end
def inspect
- entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect)
+ entries = records.take([limit_value, 11].compact.min).map!(&:inspect)
entries[10] = '...' if entries.size == 11
"#<#{self.class.name} [#{entries.join(', ')}]>"
@@ -685,14 +689,14 @@ module ActiveRecord
protected
def load_records(records)
- @records = records
+ @records = records.freeze
@loaded = true
end
private
def exec_queries
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes)
+ @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes).freeze
preload = preload_values
preload += includes_values unless eager_loading?
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index de005e2810..3639625722 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -2,6 +2,8 @@ require "active_record/relation/batches/batch_enumerator"
module ActiveRecord
module Batches
+ ORDER_OR_LIMIT_IGNORED_MESSAGE = "Scoped order and limit are ignored, it's forced to be batch order and batch size."
+
# Looping through a collection of records from the database
# (using the Scoping::Named::ClassMethods.all method, for example)
# is very inefficient since it will try to instantiate all the objects at once.
@@ -31,6 +33,9 @@ module ActiveRecord
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
+ # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
+ # the order and limit have to be ignored due to batching.
+ #
# This is especially useful if you want multiple workers dealing with
# the same processing queue. You can make worker 1 handle all the records
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
@@ -48,13 +53,13 @@ module ActiveRecord
#
# NOTE: You can't set the limit either, that's used to control
# the batch sizes.
- def find_each(start: nil, finish: nil, batch_size: 1000)
+ def find_each(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
if block_given?
- find_in_batches(start: start, finish: finish, batch_size: batch_size) do |records|
+ find_in_batches(start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do |records|
records.each { |record| yield record }
end
else
- enum_for(:find_each, start: start, finish: finish, batch_size: batch_size) do
+ enum_for(:find_each, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do
relation = self
apply_limits(relation, start, finish).size
end
@@ -83,6 +88,9 @@ module ActiveRecord
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
+ # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
+ # the order and limit have to be ignored due to batching.
+ #
# This is especially useful if you want multiple workers dealing with
# the same processing queue. You can make worker 1 handle all the records
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
@@ -100,16 +108,16 @@ module ActiveRecord
#
# NOTE: You can't set the limit either, that's used to control
# the batch sizes.
- def find_in_batches(start: nil, finish: nil, batch_size: 1000)
+ def find_in_batches(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil)
relation = self
unless block_given?
- return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size) do
+ return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size, error_on_ignore: error_on_ignore) do
total = apply_limits(relation, start, finish).size
(total - 1).div(batch_size) + 1
end
end
- in_batches(of: batch_size, start: start, finish: finish, load: true) do |batch|
+ in_batches(of: batch_size, start: start, finish: finish, load: true, error_on_ignore: error_on_ignore) do |batch|
yield batch.to_a
end
end
@@ -140,6 +148,8 @@ module ActiveRecord
# * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false.
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
+ # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
+ # the order and limit have to be ignored due to batching.
#
# This is especially useful if you want to work with the
# ActiveRecord::Relation object instead of the array of records, or if
@@ -171,14 +181,14 @@ module ActiveRecord
#
# NOTE: You can't set the limit either, that's used to control the batch
# sizes.
- def in_batches(of: 1000, start: nil, finish: nil, load: false)
+ def in_batches(of: 1000, start: nil, finish: nil, load: false, error_on_ignore: nil)
relation = self
unless block_given?
return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self)
end
- if logger && (arel.orders.present? || arel.taken.present?)
- logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ if arel.orders.present? || arel.taken.present?
+ act_on_order_or_limit_ignored(error_on_ignore)
end
relation = relation.reorder(batch_order).limit(of)
@@ -187,7 +197,7 @@ module ActiveRecord
loop do
if load
- records = batch_relation.to_a
+ records = batch_relation.records
ids = records.map(&:id)
yielded_relation = self.where(primary_key => ids)
yielded_relation.load_records(records)
@@ -219,5 +229,15 @@ module ActiveRecord
def batch_order
"#{quoted_table_name}.#{quoted_primary_key} ASC"
end
+
+ def act_on_order_or_limit_ignored(error_on_ignore)
+ raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order_or_limit : error_on_ignore)
+
+ if raise_error
+ raise ArgumentError.new(ORDER_OR_LIMIT_IGNORED_MESSAGE)
+ elsif logger
+ logger.warn(ORDER_OR_LIMIT_IGNORED_MESSAGE)
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
index c6e39814dd..13393dc605 100644
--- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
+++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb
@@ -35,7 +35,7 @@ module ActiveRecord
return to_enum(:each_record) unless block_given?
@relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation|
- relation.to_a.each { |record| yield record }
+ relation.records.each { |record| yield record }
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index e4e5d63006..f2578f5f96 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -37,7 +37,8 @@ module ActiveRecord
# for each different klass, and the delegations are compiled into that subclass only.
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join,
- :[], :&, :|, :+, :-, :sample, :shuffle, :reverse, :compact, to: :to_a
+ :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of,
+ :shuffle, :split, to: :records
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
:connection, :columns_hash, :to => :klass
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index a4280c5f33..e7e331f88d 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -42,10 +42,10 @@ module ActiveRecord
# Person.find_by(name: 'Spartacus', rating: 4)
# # returns the first item or nil.
#
- # Person.where(name: 'Spartacus', rating: 4).first_or_initialize
+ # Person.find_or_initialize_by(name: 'Spartacus', rating: 4)
# # returns the first item or returns a new instance (requires you call .save to persist against the database).
#
- # Person.where(name: 'Spartacus', rating: 4).first_or_create
+ # Person.find_or_create_by(name: 'Spartacus', rating: 4)
# # returns the first item or creates it and returns it.
#
# ==== Alternatives for #find
@@ -145,15 +145,21 @@ module ActiveRecord
#
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
def last(limit = nil)
- if limit
- if order_values.empty? && primary_key
- order(arel_attribute(primary_key).desc).limit(limit).reverse
- else
- to_a.last(limit)
- end
- else
- find_last
- end
+ return find_last(limit) if loaded? || limit_value
+
+ result = limit(limit || 1)
+ result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key
+ result = result.reverse_order!
+
+ limit ? result.reverse : result.first
+ rescue ActiveRecord::IrreversibleOrderError
+ ActiveSupport::Deprecation.warn(<<-WARNING.squish)
+ Finding a last element by loading the relation when SQL ORDER
+ can not be reversed is deprecated.
+ Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case.
+ Please call `to_a.last` if you still want to load the relation.
+ WARNING
+ find_last(limit)
end
# Same as #last but raises ActiveRecord::RecordNotFound if no record
@@ -249,13 +255,13 @@ module ActiveRecord
# Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3
# Person.where(["user_name = :u", { u: user_name }]).third_to_last
def third_to_last
- find_nth(-3)
+ find_nth_from_last 3
end
# Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record
# is found.
def third_to_last!
- find_nth!(-3)
+ find_nth_from_last 3 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
# Find the second-to-last record.
@@ -265,13 +271,13 @@ module ActiveRecord
# Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3
# Person.where(["user_name = :u", { u: user_name }]).second_to_last
def second_to_last
- find_nth(-2)
+ find_nth_from_last 2
end
# Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record
# is found.
def second_to_last!
- find_nth!(-2)
+ find_nth_from_last 2 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
# Returns true if a record exists in the table that matches the +id+ or
@@ -306,7 +312,7 @@ module ActiveRecord
conditions = conditions.id
ActiveSupport::Deprecation.warn(<<-MSG.squish)
You are passing an instance of ActiveRecord::Base to `exists?`.
- Please pass the id of the object by calling `.id`
+ Please pass the id of the object by calling `.id`.
MSG
end
@@ -330,7 +336,7 @@ module ActiveRecord
end
# This method is called whenever no records are found with either a single
- # id or multiple ids and raises a ActiveRecord::RecordNotFound exception.
+ # id or multiple ids and raises an ActiveRecord::RecordNotFound exception.
#
# The error message is different depending on whether a single id or
# multiple ids are provided. If multiple ids are provided, then the number
@@ -461,7 +467,7 @@ module ActiveRecord
id = id.id
ActiveSupport::Deprecation.warn(<<-MSG.squish)
You are passing an instance of ActiveRecord::Base to `find`.
- Please pass the id of the object by calling `.id`
+ Please pass the id of the object by calling `.id`.
MSG
end
@@ -500,7 +506,7 @@ module ActiveRecord
def find_some_ordered(ids)
ids = ids.slice(offset_value || 0, limit_value || ids.size) || []
- result = except(:limit, :offset).where(primary_key => ids).to_a
+ result = except(:limit, :offset).where(primary_key => ids).records
if result.size == ids.size
pk_type = @klass.type_for_attribute(primary_key)
@@ -516,7 +522,7 @@ module ActiveRecord
if loaded?
@records.first
else
- @take ||= limit(1).to_a.first
+ @take ||= limit(1).records.first
end
end
@@ -555,19 +561,25 @@ module ActiveRecord
relation.limit(limit).to_a
end
- def find_last
+ def find_nth_from_last(index)
if loaded?
- @records.last
+ @records[-index]
else
- @last ||=
- if limit_value
- to_a.last
- else
- reverse_order.limit(1).to_a.first
- end
+ relation = if order_values.empty? && primary_key
+ order(arel_attribute(primary_key).asc)
+ else
+ self
+ end
+
+ relation.to_a[-index]
+ # TODO: can be made more performant on large result sets by
+ # for instance, last(index)[-index] (which would require
+ # refactoring the last(n) finder method to make test suite pass),
+ # or by using a combination of reverse_order, limit, and offset,
+ # e.g., reverse_order.offset(index-1).first
end
end
-
+
private
def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc:
@@ -578,5 +590,9 @@ module ActiveRecord
find_nth_with_limit(index, limit)
end
end
+
+ def find_last(limit)
+ limit ? records.last(limit) : records.last
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 0f88791d92..ecce949370 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -5,6 +5,7 @@ module ActiveRecord
require 'active_record/relation/predicate_builder/base_handler'
require 'active_record/relation/predicate_builder/basic_object_handler'
require 'active_record/relation/predicate_builder/class_handler'
+ require 'active_record/relation/predicate_builder/polymorphic_array_handler'
require 'active_record/relation/predicate_builder/range_handler'
require 'active_record/relation/predicate_builder/relation_handler'
@@ -22,6 +23,7 @@ module ActiveRecord
register_handler(Relation, RelationHandler.new)
register_handler(Array, ArrayHandler.new(self))
register_handler(AssociationQueryValue, AssociationQueryHandler.new(self))
+ register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self))
end
def build_from_hash(attributes)
@@ -40,10 +42,7 @@ module ActiveRecord
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if table.associated_with?(column)
- value = AssociationQueryValue.new(table.associated_table(column), value)
- end
-
+ value = AssociationQueryHandler.value_for(table, column, value) if table.associated_with?(column)
build(table.arel_attribute(column), value)
end
@@ -137,9 +136,11 @@ module ActiveRecord
end
def convert_dot_notation_to_hash(attributes)
- dot_notation = attributes.keys.select { |s| s.include?(".".freeze) }
+ dot_notation = attributes.select do |k, v|
+ k.include?(".".freeze) && !v.is_a?(Hash)
+ end
- dot_notation.each do |key|
+ dot_notation.each_key do |key|
table_name, column_name = key.split(".".freeze)
value = attributes.delete(key)
attributes[table_name] ||= {}
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
index e81be63cd3..d7fd878265 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb
@@ -1,6 +1,16 @@
module ActiveRecord
class PredicateBuilder
class AssociationQueryHandler # :nodoc:
+ def self.value_for(table, column, value)
+ klass = if table.associated_table(column).polymorphic_association? && ::Array === value && value.first.is_a?(Base)
+ PolymorphicArrayValue
+ else
+ AssociationQueryValue
+ end
+
+ klass.new(table.associated_table(column), value)
+ end
+
def initialize(predicate_builder)
@predicate_builder = predicate_builder
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
new file mode 100644
index 0000000000..b6c6240343
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb
@@ -0,0 +1,57 @@
+module ActiveRecord
+ class PredicateBuilder
+ class PolymorphicArrayHandler # :nodoc:
+ def initialize(predicate_builder)
+ @predicate_builder = predicate_builder
+ end
+
+ def call(attribute, value)
+ table = value.associated_table
+ queries = value.type_to_ids_mapping.map do |type, ids|
+ { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids }
+ end
+
+ predicates = queries.map { |query| predicate_builder.build_from_hash(query) }
+
+ if predicates.size > 1
+ type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) }
+ type_and_ids_predicates.inject(&:or)
+ else
+ predicates.first
+ end
+ end
+
+ protected
+
+ attr_reader :predicate_builder
+ end
+
+ class PolymorphicArrayValue # :nodoc:
+ attr_reader :associated_table, :values
+
+ def initialize(associated_table, values)
+ @associated_table = associated_table
+ @values = values
+ end
+
+ def type_to_ids_mapping
+ default_hash = Hash.new { |hsh, key| hsh[key] = [] }
+ values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) }
+ end
+
+ private
+
+ def primary_key(value)
+ associated_table.association_primary_key(base_class(value))
+ end
+
+ def base_class(value)
+ value.class.base_class
+ end
+
+ def convert_to_id(value)
+ value._read_attribute(primary_key(value))
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 91bfa4d131..4533f3263f 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -655,6 +655,10 @@ module ActiveRecord
# # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3'))
#
def or(other)
+ unless other.is_a? Relation
+ raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
+ end
+
spawn.or!(other)
end
@@ -1112,6 +1116,8 @@ module ActiveRecord
order_query.flat_map do |o|
case o
+ when Arel::Attribute
+ o.desc
when Arel::Nodes::Ordering
o.reverse
when String
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 67d7f83cb4..d5c18a2a4a 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -29,7 +29,7 @@ module ActiveRecord
# This is mainly intended for sharing common conditions between multiple associations.
def merge(other)
if other.is_a?(Array)
- to_a & other
+ records & other
elsif other
spawn.merge!(other)
else
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index 2c2d6cfa47..89396b518c 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -158,8 +158,9 @@ module ActiveRecord
end
end
+ ARRAY_WITH_EMPTY_STRING = ['']
def non_empty_predicates
- predicates - ['']
+ predicates - ARRAY_WITH_EMPTY_STRING
end
def wrap_sql_literal(node)
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 2bfc5ff7ae..a9e1fd0dad 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -60,7 +60,7 @@ module ActiveRecord
end
# Accepts an array, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a ORDER clause.
+ # them into a valid SQL fragment for an ORDER clause.
#
# sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
# # => "field(id, 1,3,2)"
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index f115c7542b..b4229cba04 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -104,10 +104,7 @@ HEADER
end
def table(table, stream)
- columns = @connection.columns(table).map do |column|
- column.instance_variable_set(:@table_name, table)
- column
- end
+ columns = @connection.columns(table)
begin
tbl = StringIO.new
@@ -126,7 +123,7 @@ HEADER
tbl.print ", primary_key: #{pk.inspect}" unless pk == 'id'
pkcol = columns.detect { |c| c.name == pk }
pkcolspec = @connection.column_spec_for_primary_key(pkcol)
- if pkcolspec
+ if pkcolspec.present?
pkcolspec.each do |key, value|
tbl.print ", #{key}: #{value}"
end
@@ -141,6 +138,10 @@ HEADER
table_options = @connection.table_options(table)
tbl.print ", options: #{table_options.inspect}" unless table_options.blank?
+ if comment = @connection.table_comment(table)
+ tbl.print ", comment: #{comment.inspect}"
+ end
+
tbl.puts " do |t|"
# then dump all non-primary key columns
@@ -212,6 +213,7 @@ HEADER
statement_parts << "where: #{index.where.inspect}" if index.where
statement_parts << "using: #{index.using.inspect}" if index.using
statement_parts << "type: #{index.type.inspect}" if index.type
+ statement_parts << "comment: #{index.comment.inspect}" if index.comment
" #{statement_parts.join(', ')}"
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index f6b6768ce3..9eab59ac78 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -115,7 +115,8 @@ module ActiveRecord
base_rel ||= relation
evaluate_default_scope do
default_scopes.inject(base_rel) do |default_scope, scope|
- default_scope.merge(base_rel.scoping { scope.call })
+ scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
+ default_scope.merge(base_rel.instance_exec(&scope))
end
end
end
diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb
index b3644bf569..8ec4b48d31 100644
--- a/activerecord/lib/active_record/suppressor.rb
+++ b/activerecord/lib/active_record/suppressor.rb
@@ -37,7 +37,11 @@ module ActiveRecord
end
end
- def create_or_update(*args) # :nodoc:
+ def save(*) # :nodoc:
+ SuppressorRegistry.suppressed[self.class.name] ? true : super
+ end
+
+ def save!(*) # :nodoc:
SuppressorRegistry.suppressed[self.class.name] ? true : super
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 8f52e9068a..9aea5b360b 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -107,8 +107,9 @@ module ActiveRecord
def create(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).create
+ $stdout.puts "Created database '#{configuration['database']}'"
rescue DatabaseAlreadyExists
- $stderr.puts "#{configuration['database']} already exists"
+ $stderr.puts "Database '#{configuration['database']}' already exists"
rescue Exception => error
$stderr.puts error
$stderr.puts "Couldn't create database for #{configuration.inspect}"
@@ -116,7 +117,11 @@ module ActiveRecord
end
def create_all
+ old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base)
each_local_configuration { |configuration| create configuration }
+ if old_pool
+ ActiveRecord::Base.connection_handler.establish_connection(ActiveRecord::Base, old_pool.spec)
+ end
end
def create_current(environment = env)
@@ -129,11 +134,12 @@ module ActiveRecord
def drop(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).drop
+ $stdout.puts "Dropped database '#{configuration['database']}'"
rescue ActiveRecord::NoDatabaseError
$stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
$stderr.puts error
- $stderr.puts "Couldn't drop #{configuration['database']}"
+ $stderr.puts "Couldn't drop database '#{configuration['database']}'"
raise
end
@@ -155,6 +161,7 @@ module ActiveRecord
Migrator.migrate(migrations_paths, version) do |migration|
scope.blank? || scope == migration.scope
end
+ ActiveRecord::Base.clear_cache!
ensure
Migration.verbose = verbose_was
end
@@ -278,8 +285,7 @@ module ActiveRecord
def each_current_configuration(environment)
environments = [environment]
- # add test environment only if no RAILS_ENV was specified.
- environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil?
+ environments << 'test' if environment == 'development'
configurations = ActiveRecord::Base.configurations.values_at(*environments)
configurations.compact.each do |configuration|
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 7a49322e06..af0c935342 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -130,7 +130,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
'sslca' => '--ssl-ca',
'sslcert' => '--ssl-cert',
'sslcapath' => '--ssl-capath',
- 'sslcipher' => '--ssh-cipher',
+ 'sslcipher' => '--ssl-cipher',
'sslkey' => '--ssl-key'
}.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index e210e94f00..4911d93dd9 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -61,7 +61,7 @@ module ActiveRecord
register(:binary, Type::Binary, override: false)
register(:boolean, Type::Boolean, override: false)
register(:date, Type::Date, override: false)
- register(:date_time, Type::DateTime, override: false)
+ register(:datetime, Type::DateTime, override: false)
register(:decimal, Type::Decimal, override: false)
register(:float, Type::Float, override: false)
register(:integer, Type::Integer, override: false)
diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb
index 097d1bd363..513c938088 100644
--- a/activerecord/lib/active_record/type/internal/abstract_json.rb
+++ b/activerecord/lib/active_record/type/internal/abstract_json.rb
@@ -17,11 +17,7 @@ module ActiveRecord
end
def serialize(value)
- if value.is_a?(::Array) || value.is_a?(::Hash)
- ::ActiveSupport::JSON.encode(value)
- else
- value
- end
+ ::ActiveSupport::JSON.encode(value)
end
def accessor
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
index 70988d84ff..7da49e43c7 100644
--- a/activerecord/lib/active_record/type/time.rb
+++ b/activerecord/lib/active_record/type/time.rb
@@ -2,6 +2,18 @@ module ActiveRecord
module Type
class Time < ActiveModel::Type::Time
include Internal::Timezone
+
+ class Value < DelegateClass(::Time) # :nodoc:
+ end
+
+ def serialize(value)
+ case value = super
+ when ::Time
+ Value.new(value)
+ else
+ value
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 6677e6dc5f..ecaf04e39e 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -45,7 +45,7 @@ module ActiveRecord
end
# Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but
- # will raise a ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid.
+ # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid.
def save!(options={})
perform_validations(options) ? super : raise_validation_error
end
diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb
index 2e19e6dc5c..641d041f3d 100644
--- a/activerecord/lib/active_record/validations/absence.rb
+++ b/activerecord/lib/active_record/validations/absence.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Validations
class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc:
def validate_each(record, attribute, association_or_value)
- return unless should_validate?(record)
if record.class._reflect_on_association(attribute)
association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
end
diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb
index 69e048eef1..0e0cebce4a 100644
--- a/activerecord/lib/active_record/validations/length.rb
+++ b/activerecord/lib/active_record/validations/length.rb
@@ -2,23 +2,11 @@ module ActiveRecord
module Validations
class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc:
def validate_each(record, attribute, association_or_value)
- return unless should_validate?(record) || associations_are_dirty?(record)
if association_or_value.respond_to?(:loaded?) && association_or_value.loaded?
association_or_value = association_or_value.target.reject(&:marked_for_destruction?)
end
super
end
-
- def associations_are_dirty?(record)
- attributes.any? do |attribute|
- value = record.read_attribute_for_validation(attribute)
- if value.respond_to?(:loaded?) && value.loaded?
- value.target.any?(&:marked_for_destruction?)
- else
- false
- end
- end
- end
end
module ClassMethods
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 7e85ed43ac..ad82ea66c4 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Validations
class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc:
def validate_each(record, attribute, association_or_value)
- return unless should_validate?(record)
if record.class._reflect_on_association(attribute)
association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?)
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 6343de5914..1f59276137 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -11,15 +11,14 @@ module ActiveRecord
end
def validate_each(record, attribute, value)
- return unless should_validate?(record)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
value = map_enum_attribute(finder_class, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
- if record.persisted? && finder_class.primary_key.to_s != attribute.to_s
+ if record.persisted?
if finder_class.primary_key
- relation = relation.where.not(finder_class.primary_key => record.id_was)
+ relation = relation.where.not(finder_class.primary_key => record.id_was || record.id)
else
raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
end
@@ -57,14 +56,13 @@ module ActiveRecord
value = value.attributes[reflection.klass.primary_key] unless value.nil?
end
- attribute_name = attribute.to_s
-
# the attribute may be an aliased attribute
- if klass.attribute_aliases[attribute_name]
- attribute = klass.attribute_aliases[attribute_name]
- attribute_name = attribute.to_s
+ if klass.attribute_alias?(attribute)
+ attribute = klass.attribute_alias(attribute)
end
+ attribute_name = attribute.to_s
+
column = klass.columns_hash[attribute_name]
cast_type = klass.type_for_attribute(attribute_name)
value = cast_type.serialize(value)
@@ -79,7 +77,7 @@ module ActiveRecord
if value.nil?
klass.unscoped.where(comparison)
else
- bind = Relation::QueryAttribute.new(attribute.to_s, value, Type::Value.new)
+ bind = Relation::QueryAttribute.new(attribute_name, value, Type::Value.new)
klass.unscoped.where(comparison, bind)
end
rescue RangeError
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 7395839fca..f191eff5bf 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -22,11 +22,13 @@ module ActiveRecord
def create_model_file
template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb")
+ generate_application_record
end
def create_module_file
return if regular_class_path.empty?
template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke
+ generate_application_record
end
hook_for :test_framework
@@ -37,23 +39,34 @@ module ActiveRecord
attributes.select { |a| !a.reference? && a.has_index? }
end
+ # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required.
+ def generate_application_record
+ if self.behavior == :invoke && !application_record_exist?
+ template 'application_record.rb', application_record_file_name
+ end
+ end
+
# Used by the migration template to determine the parent name of the model
def parent_class_name
options[:parent] || determine_default_parent_class
end
- def determine_default_parent_class
- application_record = nil
+ def application_record_exist?
+ file_exist = nil
+ in_root { file_exist = File.exist?(application_record_file_name) }
+ file_exist
+ end
- in_root do
- application_record = if mountable_engine?
- File.exist?("app/models/#{namespaced_path}/application_record.rb")
- else
- File.exist?('app/models/application_record.rb')
- end
+ def application_record_file_name
+ @application_record_file_name ||= if mountable_engine?
+ "app/models/#{namespaced_path}/application_record.rb"
+ else
+ 'app/models/application_record.rb'
end
+ end
- if application_record
+ def determine_default_parent_class
+ if application_record_exist?
"ApplicationRecord"
else
"ActiveRecord::Base"
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb
new file mode 100644
index 0000000000..60050e0bf8
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb
@@ -0,0 +1,5 @@
+<% module_namespacing do -%>
+class ApplicationRecord < ActiveRecord::Base
+ self.abstract_class = true
+end
+<% end -%>
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 0ee147cdba..32391e2e8b 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require "models/book"
require "models/post"
require "models/author"
+require "models/event"
module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
@@ -11,7 +12,8 @@ module ActiveRecord
##
# PostgreSQL does not support null bytes in strings
- unless current_adapter?(:PostgreSQLAdapter)
+ unless current_adapter?(:PostgreSQLAdapter) ||
+ (current_adapter?(:SQLite3Adapter) && !ActiveRecord::Base.connection.prepared_statements)
def test_update_prepared_statement
b = Book.create(name: "my \x00 book")
b.reload
@@ -22,6 +24,12 @@ module ActiveRecord
end
end
+ def test_create_record_with_pk_as_zero
+ Book.create(id: 0)
+ assert_equal 0, Book.find(0).id
+ assert_nothing_raised { Book.destroy(0) }
+ end
+
def test_tables
tables = nil
ActiveSupport::Deprecation.silence { tables = @connection.tables }
@@ -193,6 +201,14 @@ module ActiveRecord
assert_not_nil error.cause
end
+
+ def test_value_limit_violations_are_translated_to_specific_exception
+ error = assert_raises(ActiveRecord::ValueTooLong) do
+ Event.create(title: 'abcdefgh')
+ end
+
+ assert_not_nil error.cause
+ end
end
def test_disable_referential_integrity
diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
index 8575df9e43..739bb275ce 100644
--- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -43,11 +43,16 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase
boolean = BooleanType.create!(archived: true, published: true)
attributes = boolean.reload.attributes_before_type_cast
-
assert_equal 1, attributes["archived"]
assert_equal "1", attributes["published"]
+ boolean = BooleanType.create!(archived: false, published: false)
+ attributes = boolean.reload.attributes_before_type_cast
+ assert_equal 0, attributes["archived"]
+ assert_equal "0", attributes["published"]
+
assert_equal 1, @connection.type_cast(true)
+ assert_equal 0, @connection.type_cast(false)
end
test "test type casting without emulated booleans" do
@@ -55,11 +60,16 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase
boolean = BooleanType.create!(archived: true, published: true)
attributes = boolean.reload.attributes_before_type_cast
-
assert_equal 1, attributes["archived"]
assert_equal "1", attributes["published"]
+ boolean = BooleanType.create!(archived: false, published: false)
+ attributes = boolean.reload.attributes_before_type_cast
+ assert_equal 0, attributes["archived"]
+ assert_equal "0", attributes["published"]
+
assert_equal 1, @connection.type_cast(true)
+ assert_equal 0, @connection.type_cast(false)
end
test "with booleans stored as 1 and 0" do
diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
index 963116f08a..9cb05119a2 100644
--- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
@@ -51,4 +51,13 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
assert_no_match(/binary/i, cs_uniqueness_query)
end
+
+ def test_case_sensitive_comparison_for_binary_column
+ CollationTest.validates_uniqueness_of(:binary_column, case_sensitive: true)
+ CollationTest.create!(binary_column: 'A')
+ invalid = CollationTest.new(binary_column: 'A')
+ queries = assert_sql { invalid.save }
+ bin_uniqueness_query = queries.detect { |q| q.match(/binary_column/) }
+ assert_no_match(/\bBINARY\b/, bin_uniqueness_query)
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 575138eb2a..c4715393b3 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -69,40 +69,48 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
def test_mysql_default_in_strict_mode
- result = @connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal [["STRICT_ALL_TABLES"]], result.rows
+ result = @connection.select_value("SELECT @@SESSION.sql_mode")
+ assert_match %r(STRICT_ALL_TABLES), result
end
def test_mysql_strict_mode_disabled
run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
- result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal [['']], result.rows
+ ActiveRecord::Base.establish_connection(orig_connection.merge(strict: false))
+ result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode")
+ assert_no_match %r(STRICT_ALL_TABLES), result
+ end
+ end
+
+ def test_mysql_strict_mode_specified_default
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.merge(strict: :default))
+ global_sql_mode = ActiveRecord::Base.connection.select_value("SELECT @@GLOBAL.sql_mode")
+ session_sql_mode = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode")
+ assert_equal global_sql_mode, session_sql_mode
+ end
+ end
+
+ def test_mysql_sql_mode_variable_overrides_strict_mode
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
+ result = ActiveRecord::Base.connection.select_value('SELECT @@SESSION.sql_mode')
+ assert_no_match %r(STRICT_ALL_TABLES), result
end
end
def test_passing_arbitary_flags_to_adapter
- run_without_connection do |orig_connection|
+ run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({flags: Mysql2::Client::COMPRESS}))
assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags]
end
end
-
+
def test_passing_flags_by_array_to_adapter
- run_without_connection do |orig_connection|
+ run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({flags: ['COMPRESS'] }))
assert_equal ["COMPRESS", "FOUND_ROWS"], ActiveRecord::Base.connection.raw_connection.query_options[:flags]
end
end
-
- def test_mysql_strict_mode_specified_default
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default}))
- global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal global_sql_mode.rows, session_sql_mode.rows
- end
- end
def test_mysql_set_session_variable
run_without_connection do |orig_connection|
@@ -112,14 +120,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
end
- def test_mysql_sql_mode_variable_overrides_strict_mode
- run_without_connection do |orig_connection|
- ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
- result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
- assert_not_equal [['STRICT_ALL_TABLES']], result.rows
- end
- end
-
def test_mysql_set_session_variable_to_default
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
new file mode 100644
index 0000000000..e349c67c93
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
@@ -0,0 +1,45 @@
+require "cases/helper"
+
+class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ test 'microsecond precision for MySQL gte 5.6.4' do
+ stub_version '5.6.4'
+ assert_microsecond_precision
+ end
+
+ test 'no microsecond precision for MySQL lt 5.6.4' do
+ stub_version '5.6.3'
+ assert_no_microsecond_precision
+ end
+
+ test 'microsecond precision for MariaDB gte 5.3.0' do
+ stub_version '5.5.5-10.1.8-MariaDB-log'
+ assert_microsecond_precision
+ end
+
+ test 'no microsecond precision for MariaDB lt 5.3.0' do
+ stub_version '5.2.9-MariaDB'
+ assert_no_microsecond_precision
+ end
+
+ private
+ def assert_microsecond_precision
+ assert_match_quoted_microsecond_datetime(/\.000001\z/)
+ end
+
+ def assert_no_microsecond_precision
+ assert_match_quoted_microsecond_datetime(/\d\z/)
+ end
+
+ def assert_match_quoted_microsecond_datetime(match)
+ assert_match match, @connection.quoted_date(Time.now.change(usec: 1))
+ end
+
+ def stub_version(full_version_string)
+ @connection.stubs(:full_version).returns(full_version_string)
+ @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
index 4fc7414b18..b783b5fcd9 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -2,26 +2,20 @@ require "cases/helper"
require 'models/developer'
require 'models/computer'
-module ActiveRecord
- module ConnectionAdapters
- class Mysql2Adapter
- class ExplainTest < ActiveRecord::Mysql2TestCase
- fixtures :developers
+class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase
+ fixtures :developers
- def test_explain_for_one_query
- explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %r(developers |.* const), explain
- end
+ def test_explain_for_one_query
+ explain = Developer.where(id: 1).explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
+ end
- def test_explain_with_eager_loading
- explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %r(developers |.* const), explain
- assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain
- assert_match %r(audit_logs |.* ALL), explain
- end
- end
- end
+ def test_explain_with_eager_loading
+ explain = Developer.where(id: 1).includes(:audit_logs).explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain
+ assert_match %r(audit_logs |.* ALL), explain
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb
index c8c933af5e..9c3fef1b59 100644
--- a/activerecord/test/cases/adapters/mysql2/json_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/json_test.rb
@@ -161,12 +161,19 @@ class Mysql2JSONTest < ActiveRecord::Mysql2TestCase
assert_not json.changed?
end
- def test_assigning_invalid_json
- json = JsonDataType.new
+ def test_assigning_string_literal
+ json = JsonDataType.create(payload: "foo")
+ assert_equal "foo", json.payload
+ end
- json.payload = 'foo'
+ def test_assigning_number
+ json = JsonDataType.create(payload: 1.234)
+ assert_equal 1.234, json.payload
+ end
- assert_nil json.payload
+ def test_assigning_boolean
+ json = JsonDataType.create(payload: true)
+ assert_equal true, json.payload
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
index 4efd728754..00d23740b6 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -1,10 +1,22 @@
require "cases/helper"
+require "support/ddl_helper"
class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
+ include DdlHelper
+
def setup
@conn = ActiveRecord::Base.connection
end
+ def test_exec_query_nothing_raises_with_no_result_queries
+ assert_nothing_raised do
+ with_example_table do
+ @conn.exec_query('INSERT INTO ex (number) VALUES (1)')
+ @conn.exec_query('DELETE FROM ex WHERE number = 1')
+ end
+ end
+ end
+
def test_columns_for_distinct_zero_orders
assert_equal "posts.id",
@conn.columns_for_distinct("posts.id", [])
@@ -41,4 +53,10 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
assert_equal "posts.id, posts.created_at AS alias_0",
@conn.columns_for_distinct("posts.id", [order])
end
+
+ private
+
+ def with_example_table(definition = 'id int auto_increment primary key, number int, data varchar(255)', &block)
+ super(@conn, 'ex', definition, &block)
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb
deleted file mode 100644
index 2de7e1b526..0000000000
--- a/activerecord/test/cases/adapters/mysql2/quoting_test.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-require "cases/helper"
-
-class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- end
-
- test 'quoted date precision for gte 5.6.4' do
- @connection.stubs(:full_version).returns('5.6.4')
- @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
- t = Time.now.change(usec: 1)
- assert_match(/\.000001\z/, @connection.quoted_date(t))
- end
-
- test 'quoted date precision for lt 5.6.4' do
- @connection.stubs(:full_version).returns('5.6.3')
- @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
- t = Time.now.change(usec: 1)
- assert_no_match(/\.000001\z/, @connection.quoted_date(t))
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
index c95a64cc16..0a9703263e 100644
--- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -58,7 +58,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
test "schema dump includes unsigned option" do
schema = dump_table_schema "unsigned_types"
assert_match %r{t.integer\s+"unsigned_integer",\s+unsigned: true$}, schema
- assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\s+unsigned: true$}, schema
+ assert_match %r{t.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema
assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema
assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema
end
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
index 6f72fa6e0f..cec6081aec 100644
--- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -57,9 +57,11 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output
end
- def test_assigning_invalid_hex_string_raises_exception
- assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" }
- assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" }
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_assigning_invalid_hex_string_raises_exception
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" }
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "F" }
+ end
end
def test_roundtrip
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index b6bb1929e6..7adc070430 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -1,6 +1,9 @@
require "cases/helper"
+require 'support/schema_dumping_helper'
class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
class ByteaDataType < ActiveRecord::Base
self.table_name = 'bytea_data_type'
end
@@ -122,4 +125,10 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
obj.reload
assert_equal "hello world", obj.serialized
end
+
+ def test_schema_dumping
+ output = dump_table_schema("bytea_data_type")
+ assert_match %r{t\.binary\s+"payload"$}, output
+ assert_match %r{t\.binary\s+"serialized"$}, output
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index d559de3e28..f8403bfe1a 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -125,14 +125,16 @@ module ActiveRecord
assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
- def test_statement_key_is_logged
- bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new)
- @connection.exec_query('SELECT $1::integer', 'SQL', [bind], prepare: true)
- name = @subscriber.payloads.last[:statement_name]
- assert name
- res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)")
- plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first
- assert_operator plan.length, :>, 0
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_statement_key_is_logged
+ bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new)
+ @connection.exec_query('SELECT $1::integer', 'SQL', [bind], prepare: true)
+ name = @subscriber.payloads.last[:statement_name]
+ assert name
+ res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)")
+ plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first
+ assert_operator plan.length, :>, 0
+ end
end
# Must have PostgreSQL >= 9.2, or with_manual_interventions set to
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 4d0fd640aa..29bf2c15ea 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -6,15 +6,15 @@ class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase
fixtures :developers
def test_explain_for_one_query
- explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
+ explain = Developer.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain
assert_match %(QUERY PLAN), explain
end
def test_explain_with_eager_loading
- explain = Developer.where(:id => 1).includes(:audit_logs).explain
+ explain = Developer.where(id: 1).includes(:audit_logs).explain
assert_match %(QUERY PLAN), explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
+ assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain
assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index b3b121b4fb..663de680b5 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -38,7 +38,7 @@ module PostgresqlJSONSharedTestCases
end
def test_default
- @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}'
+ @connection.add_column 'json_data_type', 'permissions', column_type, default: {"users": "read", "posts": ["read", "write"]}
JsonDataType.reset_column_information
assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions'])
@@ -178,12 +178,19 @@ module PostgresqlJSONSharedTestCases
assert_not json.changed?
end
- def test_assigning_invalid_json
- json = JsonDataType.new
+ def test_assigning_string_literal
+ json = JsonDataType.create(payload: "foo")
+ assert_equal "foo", json.payload
+ end
- json.payload = 'foo'
+ def test_assigning_number
+ json = JsonDataType.create(payload: 1.234)
+ assert_equal 1.234, json.payload
+ end
- assert_nil json.payload
+ def test_assigning_boolean
+ json = JsonDataType.create(payload: true)
+ assert_equal true, json.payload
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index 31e87722d9..8b08ebc3c4 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -5,6 +5,7 @@ require 'support/connection_helper'
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase
+ self.use_transactional_tests = false
include DdlHelper
include ConnectionHelper
@@ -59,50 +60,6 @@ module ActiveRecord
end
end
- def test_insert_sql_with_proprietary_returning_clause
- with_example_table do
- id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number")
- assert_equal 5150, id
- end
- end
-
- def test_insert_sql_with_quoted_schema_and_table_name
- with_example_table do
- id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)')
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
- end
- end
-
- def test_insert_sql_with_no_space_after_table_name
- with_example_table do
- id = @connection.insert_sql("insert into ex(number) values(5150)")
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
- end
- end
-
- def test_multiline_insert_sql
- with_example_table do
- id = @connection.insert_sql(<<-SQL)
- insert into ex(
- number)
- values(
- 5152
- )
- SQL
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
- end
- end
-
- def test_insert_sql_with_returning_disabled
- connection = connection_without_insert_returning
- id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)")
- expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first
- assert_equal expect.to_i, id
- end
-
def test_exec_insert_with_returning_disabled
connection = connection_without_insert_returning
result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq')
@@ -239,30 +196,6 @@ module ActiveRecord
@connection.drop_table 'ex2', if_exists: true
end
- def test_exec_insert_number
- with_example_table do
- insert(@connection, 'number' => 10)
-
- result = @connection.exec_query('SELECT number FROM ex WHERE number = 10')
-
- assert_equal 1, result.rows.length
- assert_equal 10, result.rows.last.last
- end
- end
-
- def test_exec_insert_string
- with_example_table do
- str = 'いただきます!'
- insert(@connection, 'number' => 10, 'data' => str)
-
- result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10')
-
- value = result.rows.last.last
-
- assert_equal str, value
- end
- end
-
def test_table_alias_length
assert_nothing_raised do
@connection.table_alias_length
@@ -286,33 +219,35 @@ module ActiveRecord
end
end
- def test_exec_with_binds
- with_example_table do
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [bind_param(1)])
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_exec_with_binds
+ with_example_table do
+ string = @connection.quote('foo')
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ bind = Relation::QueryAttribute.new("id", 1, Type::Value.new)
+ result = @connection.exec_query('SELECT id, data FROM ex WHERE id = $1', nil, [bind])
- assert_equal [[1, 'foo']], result.rows
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
+ end
end
- end
- def test_exec_typecasts_bind_vals
- with_example_table do
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ def test_exec_typecasts_bind_vals
+ with_example_table do
+ string = @connection.quote('foo')
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new)
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [bind])
+ bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)
+ result = @connection.exec_query('SELECT id, data FROM ex WHERE id = $1', nil, [bind])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
end
@@ -438,19 +373,6 @@ module ActiveRecord
end
private
- def insert(ctx, data)
- binds = data.map { |name, value|
- bind_param(value, name)
- }
- columns = binds.map(&:name)
-
- bind_subs = columns.length.times.map { |x| "$#{x + 1}" }
-
- sql = "INSERT INTO ex (#{columns.join(", ")})
- VALUES (#{bind_subs.join(', ')})"
-
- ctx.exec_insert(sql, 'SQL', binds)
- end
def with_example_table(definition = 'id serial primary key, number integer, data character varying(255)', &block)
super(@connection, 'ex', definition, &block)
@@ -459,10 +381,6 @@ module ActiveRecord
def connection_without_insert_returning
ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false))
end
-
- def bind_param(value, name = nil)
- ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new)
- end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb
new file mode 100644
index 0000000000..f1519db48b
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb
@@ -0,0 +1,22 @@
+require "cases/helper"
+require "models/developer"
+
+class PreparedStatementsTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :developers
+
+ def setup
+ @default_prepared_statements = Developer.connection_config[:prepared_statements]
+ Developer.connection_config[:prepared_statements] = false
+ end
+
+ def teardown
+ Developer.connection_config[:prepared_statements] = @default_prepared_statements
+ end
+
+ def nothing_raised_with_falsy_prepared_statements
+ assert_nothing_raised do
+ Developer.where(id: 1)
+ end
+ end
+
+end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index a0afd922b2..285a92f60e 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -55,20 +55,22 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase
set_session_auth
USERS.each do |u|
set_session_auth u
- assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name']
+ assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1")
set_session_auth
end
end
end
- def test_auth_with_bind
- assert_nothing_raised do
- set_session_auth
- USERS.each do |u|
- @connection.clear_cache!
- set_session_auth u
- assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name']
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_auth_with_bind
+ assert_nothing_raised do
set_session_auth
+ USERS.each do |u|
+ @connection.clear_cache!
+ set_session_auth u
+ assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)])
+ set_session_auth
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index f50fe88b9b..00ebabc9c5 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -172,16 +172,18 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
end
- def test_schema_change_with_prepared_stmt
- altered = false
- @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
- @connection.exec_query "alter table developers add column zomg int", 'sql', []
- altered = true
- @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
- ensure
- # We are not using DROP COLUMN IF EXISTS because that syntax is only
- # supported by pg 9.X
- @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_schema_change_with_prepared_stmt
+ altered = false
+ @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
+ @connection.exec_query "alter table developers add column zomg int", 'sql', []
+ altered = true
+ @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)]
+ ensure
+ # We are not using DROP COLUMN IF EXISTS because that syntax is only
+ # supported by pg 9.X
+ @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered
+ end
end
def test_data_source_exists?
diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb
index 7d30db247b..8abe064bf1 100644
--- a/activerecord/test/cases/adapters/postgresql/serial_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb
@@ -10,6 +10,7 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
@connection.create_table "postgresql_serials", force: true do |t|
t.serial :seq
+ t.integer :serials_id, default: -> { "nextval('postgresql_serials_id_seq')" }
end
end
@@ -24,9 +25,21 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
assert column.serial?
end
+ def test_not_serial_column
+ column = PostgresqlSerial.columns_hash["serials_id"]
+ assert_equal :integer, column.type
+ assert_equal "integer", column.sql_type
+ assert_not column.serial?
+ end
+
def test_schema_dump_with_shorthand
output = dump_table_schema "postgresql_serials"
- assert_match %r{t\.serial\s+"seq"}, output
+ assert_match %r{t\.serial\s+"seq",\s+null: false$}, output
+ end
+
+ def test_schema_dump_with_not_serial
+ output = dump_table_schema "postgresql_serials"
+ assert_match %r{t\.integer\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_serials_id_seq'::regclass\)" \}$}, output
end
end
@@ -39,6 +52,7 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
@connection.create_table "postgresql_big_serials", force: true do |t|
t.bigserial :seq
+ t.bigint :serials_id, default: -> { "nextval('postgresql_big_serials_id_seq')" }
end
end
@@ -53,8 +67,20 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
assert column.serial?
end
+ def test_not_bigserial_column
+ column = PostgresqlBigSerial.columns_hash["serials_id"]
+ assert_equal :integer, column.type
+ assert_equal "bigint", column.sql_type
+ assert_not column.serial?
+ end
+
def test_schema_dump_with_shorthand
output = dump_table_schema "postgresql_big_serials"
- assert_match %r{t\.bigserial\s+"seq"}, output
+ assert_match %r{t\.bigserial\s+"seq",\s+null: false$}, output
+ end
+
+ def test_schema_dump_with_not_bigserial
+ output = dump_table_schema "postgresql_big_serials"
+ assert_match %r{t\.bigint\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_big_serials_id_seq'::regclass\)" \}$}, output
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
index 2aec322582..a1a6e5f16a 100644
--- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -2,26 +2,20 @@ require "cases/helper"
require 'models/developer'
require 'models/computer'
-module ActiveRecord
- module ConnectionAdapters
- class SQLite3Adapter
- class ExplainTest < ActiveRecord::SQLite3TestCase
- fixtures :developers
+class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase
+ fixtures :developers
- def test_explain_for_one_query
- explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- end
+ def test_explain_for_one_query
+ explain = Developer.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain
+ assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
+ end
- def test_explain_with_eager_loading
- explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
- assert_match(/(SCAN )?TABLE audit_logs/, explain)
- end
- end
- end
+ def test_explain_with_eager_loading
+ explain = Developer.where(id: 1).includes(:audit_logs).explain
+ assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain
+ assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
+ assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain
+ assert_match(/(SCAN )?TABLE audit_logs/, explain)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 87a892db37..f3ec2b98d3 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -8,9 +8,7 @@ module ActiveRecord
class SQLite3Adapter
class QuotingTest < ActiveRecord::SQLite3TestCase
def setup
- @conn = Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => 100
+ @conn = ActiveRecord::Base.connection
end
def test_type_cast_binary_encoding_without_logger
@@ -89,6 +87,13 @@ module ActiveRecord
assert_equal "'hello'", @conn.quote(type.serialize(value))
end
+
+ def test_quoted_time_returns_date_qualified_time
+ value = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999)
+ type = Type::Time.new
+
+ assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value))
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 02c3358ba6..bbc9f978bf 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -213,24 +213,12 @@ module ActiveRecord
assert_equal "''", @conn.quote_string("'")
end
- def test_insert_sql
- with_example_table do
- 2.times do |i|
- rv = @conn.insert_sql "INSERT INTO ex (number) VALUES (#{i})"
- assert_equal(i + 1, rv)
- end
-
- records = @conn.execute "SELECT * FROM ex"
- assert_equal 2, records.length
- end
- end
-
- def test_insert_sql_logged
+ def test_insert_logged
with_example_table do
sql = "INSERT INTO ex (number) VALUES (10)"
name = "foo"
assert_logged [[sql, name, []]] do
- @conn.insert_sql sql, name
+ @conn.insert(sql, name)
end
end
end
@@ -239,7 +227,7 @@ module ActiveRecord
with_example_table do
sql = "INSERT INTO ex (number) VALUES (10)"
idval = 'vuvuzela'
- id = @conn.insert_sql sql, nil, nil, idval
+ id = @conn.insert(sql, nil, nil, idval)
assert_equal idval, id
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 4f99c57c3c..a3046d526e 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -726,7 +726,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert companies(:first_client).readonly_firm.readonly?
end
- def test_test_polymorphic_assignment_foreign_key_type_string
+ def test_polymorphic_assignment_foreign_key_type_string
comment = Comment.first
comment.author = Author.first
comment.resource = Member.first
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 874d53c51f..7f2a2229ee 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -749,6 +749,38 @@ class EagerAssociationTest < ActiveRecord::TestCase
}
end
+ def test_eager_has_many_through_with_order
+ tag = OrderedTag.create(name: 'Foo')
+ post1 = Post.create!(title: 'Beaches', body: "I like beaches!")
+ post2 = Post.create!(title: 'Pools', body: "I like pools!")
+
+ Tagging.create!(taggable_type: 'Post', taggable_id: post1.id, tag: tag)
+ Tagging.create!(taggable_type: 'Post', taggable_id: post2.id, tag: tag)
+
+ tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id)
+ assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title))
+ end
+
+ def test_eager_has_many_through_multiple_with_order
+ tag1 = OrderedTag.create!(name: 'Bar')
+ tag2 = OrderedTag.create!(name: 'Foo')
+
+ post1 = Post.create!(title: 'Beaches', body: "I like beaches!")
+ post2 = Post.create!(title: 'Pools', body: "I like pools!")
+
+ Tagging.create!(taggable: post1, tag: tag1)
+ Tagging.create!(taggable: post2, tag: tag1)
+ Tagging.create!(taggable: post2, tag: tag2)
+ Tagging.create!(taggable: post1, tag: tag2)
+
+ tags_with_includes = OrderedTag.where(id: [tag1, tag2].map(&:id)).includes(:tagged_posts).order(:id).to_a
+ tag1_with_includes = tags_with_includes.first
+ tag2_with_includes = tags_with_includes.last
+
+ assert_equal([post2, post1].map(&:title), tag1_with_includes.tagged_posts.map(&:title))
+ assert_equal([post1, post2].map(&:title), tag2_with_includes.tagged_posts.map(&:title))
+ end
+
def test_eager_with_default_scope
developer = EagerDeveloperWithDefaultScope.where(:name => 'David').first
projects = Project.order(:id).to_a
@@ -1216,7 +1248,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_join_eager_with_empty_order_should_generate_valid_sql
- assert_nothing_raised(ActiveRecord::StatementInvalid) do
+ assert_nothing_raised do
Post.includes(:comments).order("").where(:comments => {:body => "Thank you for the welcome"}).first
end
end
@@ -1360,6 +1392,18 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal('10 was not recognized for preload', exception.message)
end
+ test "associations with extensions are not instance dependent" do
+ assert_nothing_raised do
+ Author.includes(:posts_with_extension).to_a
+ end
+ end
+
+ test "including associations with extensions and an instance dependent scope is not supported" do
+ e = assert_raises(ArgumentError) do
+ Author.includes(:posts_with_extension_and_instance).to_a
+ end
+ assert_match(/Preloading instance dependent scopes is not supported/, e.message)
+ end
test "preloading readonly association" do
# has-one
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index 5c4586da19..1bbca84bb2 100644
--- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
@@ -146,6 +146,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 1, country.treaties.count
end
+ def test_join_table_composite_primary_key_should_not_warn
+ country = Country.new(:name => 'India')
+ country.country_id = 'c1'
+ country.save!
+
+ treaty = Treaty.new(:name => 'peace')
+ treaty.treaty_id = 't1'
+ warning = capture(:stderr) do
+ country.treaties << treaty
+ end
+ assert_no_match(/WARNING: Rails does not support composite primary key\./, warning)
+ end
+
def test_has_and_belongs_to_many
david = Developer.find(1)
@@ -925,7 +938,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_with_symbol_class_name
- assert_nothing_raised NoMethodError do
+ assert_nothing_raised do
DeveloperWithSymbolClassName.new
end
end
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 bb8c9fa19c..aff0dabee7 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -11,7 +11,9 @@ require 'models/tagging'
require 'models/author'
require 'models/owner'
require 'models/pet'
+require 'models/pet_treasure'
require 'models/toy'
+require 'models/treasure'
require 'models/contract'
require 'models/company'
require 'models/developer'
@@ -1082,6 +1084,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [], person.posts
end
+ def test_preloading_empty_through_with_polymorphic_source_association
+ owner = Owner.create!(name: "Rainbow Unicat")
+ pet = Pet.create!(owner: owner)
+ person = Person.create!(first_name: "Gaga")
+ treasure = Treasure.create!(looter: person)
+ non_looted_treasure = Treasure.create!()
+ PetTreasure.create!(pet: pet, treasure: treasure, rainbow_color: "Ultra violet indigo")
+ PetTreasure.create!(pet: pet, treasure: non_looted_treasure, rainbow_color: "Ultra violet indigo")
+
+ assert_equal [person], Owner.where(name: "Rainbow Unicat").includes(pets: :persons).first.persons.to_a
+ end
+
def test_explicitly_joining_join_table
assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 57d1c8feda..c9743e80d3 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -130,15 +130,15 @@ end
class InverseAssociationTests < ActiveRecord::TestCase
def test_should_allow_for_inverse_of_options_in_associations
- assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_many') do
+ assert_nothing_raised do
Class.new(ActiveRecord::Base).has_many(:wheels, :inverse_of => :car)
end
- assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_one') do
+ assert_nothing_raised do
Class.new(ActiveRecord::Base).has_one(:engine, :inverse_of => :car)
end
- assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on belongs_to') do
+ assert_nothing_raised do
Class.new(ActiveRecord::Base).belongs_to(:car, :inverse_of => :driver)
end
end
@@ -666,7 +666,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error
# Ideally this would, if only for symmetry's sake with other association types
- assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man }
+ assert_nothing_raised { Face.first.horrible_polymorphic_man }
end
def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error
@@ -676,7 +676,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error
# passes because Man does have the correct inverse_of
- assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Man.first }
+ assert_nothing_raised { Face.first.polymorphic_man = Man.first }
# fails because Interest does have the correct inverse_of
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first }
end
@@ -688,7 +688,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
fixtures :men, :interests, :zines
def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models
- assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do
+ assert_nothing_raised do
i = Interest.first
i.zine
i.man
@@ -696,7 +696,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
end
def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models
- assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do
+ assert_nothing_raised do
i = Interest.first
i.build_zine(:title => 'Get Some in Winter! 2008')
i.build_man(:name => 'Gordon')
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index f6dddaf5b4..c7bd9d2119 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -88,7 +88,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins
assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first
- assert_nothing_raised(NoMethodError) { tag.author_id }
+ assert_nothing_raised { tag.author_id }
end
def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key
@@ -363,6 +363,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_equal posts(:welcome, :thinking).sort_by(&:id), tags(:general).tagged_posts.sort_by(&:id)
end
+ def test_has_many_polymorphic_associations_merges_through_scope
+ Tag.has_many :null_taggings, -> { none }, class_name: :Tagging
+ Tag.has_many :null_tagged_posts, :through => :null_taggings, :source => 'taggable', :source_type => 'Post'
+ assert_equal [], tags(:general).null_tagged_posts
+ refute_equal [], tags(:general).tagged_posts
+ end
+
def test_eager_has_many_polymorphic_with_source_type
tag_with_include = Tag.all.merge!(:includes => :tagged_posts).find(tags(:general).id)
desired = posts(:welcome, :thinking)
@@ -740,6 +747,23 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert_equal aircraft.engines, [engine]
end
+ def test_proper_error_message_for_eager_load_and_includes_association_errors
+ includes_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.includes(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_error.message)
+
+ eager_load_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.eager_load(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", eager_load_error.message)
+
+ includes_and_eager_load_error = assert_raises(ActiveRecord::ConfigurationError) {
+ Post.eager_load(:nonexistent_relation).includes(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1)
+ }
+ assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_and_eager_load_error.message)
+ end
+
private
# create dynamic Post models to allow different dependency options
def find_post_with_dependency(post_id, association, association_name, dependency)
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index 2991ca8b76..2bebbfa205 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -135,6 +135,17 @@ module ActiveRecord
assert_equal 2, klass.new.counter
end
+ test "procs are memoized before type casting" do
+ klass = Class.new(OverloadedType) do
+ @@counter = 0
+ attribute :counter, :integer, default: -> { @@counter += 1 }
+ end
+
+ model = klass.new
+ assert_equal 1, model.counter_before_type_cast
+ assert_equal 1, model.counter_before_type_cast
+ end
+
test "user provided defaults are persisted even if unchanged" do
model = OverloadedType.create!
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 3608063b01..9e3266b7d6 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -749,7 +749,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
# has_one
- def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
+ def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
assert !@pirate.ship.marked_for_destruction?
@pirate.ship.mark_for_destruction
@@ -809,7 +809,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
# belongs_to
- def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
+ def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
assert !@ship.pirate.marked_for_destruction?
@ship.pirate.mark_for_destruction
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 3602ee7ba2..91ff5146fd 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -108,7 +108,7 @@ class EachTest < ActiveRecord::TestCase
end
end
- def test_find_in_batches_should_finish_the_end_option
+ def test_find_in_batches_should_end_at_the_finish_option
assert_queries(6) do
Post.find_in_batches(batch_size: 1, finish: 5) do |batch|
assert_kind_of Array, batch
@@ -164,6 +164,42 @@ class EachTest < ActiveRecord::TestCase
assert_equal posts(:welcome).id, posts.first.id
end
+ def test_find_in_batches_should_error_on_ignore_the_order
+ assert_raise(ArgumentError) do
+ PostWithDefaultScope.find_in_batches(error_on_ignore: true){}
+ end
+ end
+
+ def test_find_in_batches_should_not_error_if_config_overriden
+ # Set the config option which will be overriden
+ prev = ActiveRecord::Base.error_on_ignored_order_or_limit
+ ActiveRecord::Base.error_on_ignored_order_or_limit = true
+ assert_nothing_raised do
+ PostWithDefaultScope.find_in_batches(error_on_ignore: false){}
+ end
+ ensure
+ # Set back to default
+ ActiveRecord::Base.error_on_ignored_order_or_limit = prev
+ end
+
+ def test_find_in_batches_should_error_on_config_specified_to_error
+ # Set the config option
+ prev = ActiveRecord::Base.error_on_ignored_order_or_limit
+ ActiveRecord::Base.error_on_ignored_order_or_limit = true
+ assert_raise(ArgumentError) do
+ PostWithDefaultScope.find_in_batches(){}
+ end
+ ensure
+ # Set back to default
+ ActiveRecord::Base.error_on_ignored_order_or_limit = prev
+ end
+
+ def test_find_in_batches_should_not_error_by_default
+ assert_nothing_raised do
+ PostWithDefaultScope.find_in_batches(){}
+ end
+ end
+
def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order
special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort
posts = []
@@ -316,7 +352,7 @@ class EachTest < ActiveRecord::TestCase
end
end
- def test_in_batches_should_finish_the_end_option
+ def test_in_batches_should_end_at_the_finish_option
post = Post.order('id DESC').where('id <= ?', 5).first
assert_queries(7) do
relation = Post.in_batches(of: 1, finish: 5, load: true).reverse_each.first
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index cd9c76f1f0..fa924fe4cb 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -31,7 +31,8 @@ module ActiveRecord
ActiveSupport::Notifications.unsubscribe(@subscription)
end
- if ActiveRecord::Base.connection.supports_statement_cache?
+ if ActiveRecord::Base.connection.supports_statement_cache? &&
+ ActiveRecord::Base.connection.prepared_statements
def test_bind_from_join_in_subquery
subquery = Author.joins(:thinking_posts).where(name: 'David')
scope = Author.from(subquery, 'authors').where(id: 1)
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index c922a8d1c2..8f2682c781 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -124,7 +124,7 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_generate_valid_sql_with_joins_and_group
- assert_nothing_raised ActiveRecord::StatementInvalid do
+ assert_nothing_raised do
AuditLog.joins(:developer).group(:id).count
end
end
@@ -742,7 +742,7 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association
- assert_nothing_raised ActiveRecord::StatementInvalid do
+ assert_nothing_raised do
developer = Developer.create!(name: 'developer')
developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count
end
diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb
new file mode 100644
index 0000000000..cb6f07c925
--- /dev/null
+++ b/activerecord/test/cases/comment_test.rb
@@ -0,0 +1,89 @@
+require 'cases/helper'
+require 'support/schema_dumping_helper'
+
+class CommentTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false if current_adapter?(:Mysql2Adapter)
+
+ class Commented < ActiveRecord::Base
+ self.table_name = 'commenteds'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ @connection.transaction do
+ @connection.create_table('commenteds', comment: 'A table with comment', force: true) do |t|
+ t.string 'name', comment: 'Comment should help clarify the column purpose'
+ t.boolean 'obvious', comment: 'Question is: should you comment obviously named objects?'
+ t.string 'content'
+ t.index 'name', comment: %Q["Very important" index that powers all the performance.\nAnd it's fun!]
+ end
+ end
+ end
+
+ teardown do
+ @connection.drop_table 'commenteds', if_exists: true
+ end
+
+ if ActiveRecord::Base.connection.supports_comments?
+ def test_column_created_in_block
+ Commented.reset_column_information
+ column = Commented.columns_hash['name']
+ assert_equal :string, column.type
+ assert_equal 'Comment should help clarify the column purpose', column.comment
+ end
+
+ def test_add_column_with_comment_later
+ @connection.add_column :commenteds, :rating, :integer, comment: 'I am running out of imagination'
+ Commented.reset_column_information
+ column = Commented.columns_hash['rating']
+
+ assert_equal :integer, column.type
+ assert_equal 'I am running out of imagination', column.comment
+ end
+
+ def test_add_index_with_comment_later
+ @connection.add_index :commenteds, :obvious, name: 'idx_obvious', comment: 'We need to see obvious comments'
+ index = @connection.indexes('commenteds').find { |idef| idef.name == 'idx_obvious' }
+ assert_equal 'We need to see obvious comments', index.comment
+ end
+
+ def test_add_comment_to_column
+ @connection.change_column :commenteds, :content, :string, comment: 'Whoa, content describes itself!'
+
+ Commented.reset_column_information
+ column = Commented.columns_hash['content']
+
+ assert_equal :string, column.type
+ assert_equal 'Whoa, content describes itself!', column.comment
+ end
+
+ def test_remove_comment_from_column
+ @connection.change_column :commenteds, :obvious, :string, comment: nil
+
+ Commented.reset_column_information
+ column = Commented.columns_hash['obvious']
+
+ assert_equal :string, column.type
+ assert_nil column.comment
+ end
+
+ def test_schema_dump_with_comments
+ # Do all the stuff from other tests
+ @connection.add_column :commenteds, :rating, :integer, comment: 'I am running out of imagination'
+ @connection.change_column :commenteds, :content, :string, comment: 'Whoa, content describes itself!'
+ @connection.change_column :commenteds, :obvious, :string, comment: nil
+ @connection.add_index :commenteds, :obvious, name: 'idx_obvious', comment: 'We need to see obvious comments'
+ # And check that these changes are reflected in dump
+ output = dump_table_schema 'commenteds'
+ assert_match %r[create_table "commenteds",.+\s+comment: "A table with comment"], output
+ assert_match %r[t\.string\s+"name",\s+comment: "Comment should help clarify the column purpose"], output
+ assert_match %r[t\.string\s+"obvious"\n], output
+ assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output
+ assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output
+ assert_match %r[add_index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output
+ assert_match %r[add_index\s+.+\s+name: "idx_obvious",.+\s+comment: "We need to see obvious comments"], output
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index d43668e57c..c4c2c69d1c 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -19,7 +19,7 @@ module ActiveRecord
def setup
@env = {}
@app = App.new
- @management = ConnectionManagement.new(@app)
+ @management = middleware(@app)
# make sure we have an active connection
assert ActiveRecord::Base.connection
@@ -27,17 +27,12 @@ module ActiveRecord
end
def test_app_delegation
- manager = ConnectionManagement.new(@app)
+ manager = middleware(@app)
manager.call @env
assert_equal [@env], @app.calls
end
- def test_connections_are_active_after_call
- @management.call(@env)
- assert ActiveRecord::Base.connection_handler.active_connections?
- end
-
def test_body_responds_to_each
_, _, body = @management.call(@env)
bits = []
@@ -52,45 +47,40 @@ module ActiveRecord
end
def test_active_connections_are_not_cleared_on_body_close_during_test
- @env['rack.test'] = true
- _, _, body = @management.call(@env)
- body.close
- assert ActiveRecord::Base.connection_handler.active_connections?
+ executor.wrap do
+ _, _, body = @management.call(@env)
+ body.close
+ assert ActiveRecord::Base.connection_handler.active_connections?
+ end
end
def test_connections_closed_if_exception
app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
- explosive = ConnectionManagement.new(app)
+ explosive = middleware(app)
assert_raises(NotImplementedError) { explosive.call(@env) }
assert !ActiveRecord::Base.connection_handler.active_connections?
end
def test_connections_not_closed_if_exception_and_test
- @env['rack.test'] = true
- app = Class.new(App) { def call(env); raise; end }.new
- explosive = ConnectionManagement.new(app)
- assert_raises(RuntimeError) { explosive.call(@env) }
- assert ActiveRecord::Base.connection_handler.active_connections?
- end
-
- def test_connections_closed_if_exception_and_explicitly_not_test
- @env['rack.test'] = false
- app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
- explosive = ConnectionManagement.new(app)
- assert_raises(NotImplementedError) { explosive.call(@env) }
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ executor.wrap do
+ app = Class.new(App) { def call(env); raise; end }.new
+ explosive = middleware(app)
+ assert_raises(RuntimeError) { explosive.call(@env) }
+ assert ActiveRecord::Base.connection_handler.active_connections?
+ end
end
test "doesn't clear active connections when running in a test case" do
- @env['rack.test'] = true
- @management.call(@env)
- assert ActiveRecord::Base.connection_handler.active_connections?
+ executor.wrap do
+ @management.call(@env)
+ assert ActiveRecord::Base.connection_handler.active_connections?
+ end
end
test "proxy is polite to its body and responds to it" do
body = Class.new(String) { def to_path; "/path"; end }.new
app = lambda { |_| [200, {}, body] }
- response_body = ConnectionManagement.new(app).call(@env)[2]
+ response_body = middleware(app).call(@env)[2]
assert response_body.respond_to?(:to_path)
assert_equal "/path", response_body.to_path
end
@@ -98,9 +88,23 @@ module ActiveRecord
test "doesn't mutate the original response" do
original_response = [200, {}, 'hi']
app = lambda { |_| original_response }
- ConnectionManagement.new(app).call(@env)[2]
+ middleware(app).call(@env)[2]
assert_equal 'hi', original_response.last
end
+
+ private
+ def executor
+ @executor ||= Class.new(ActiveSupport::Executor).tap do |exe|
+ ActiveRecord::QueryCache.install_executor_hooks(exe)
+ end
+ end
+
+ def middleware(app)
+ lambda do |env|
+ a, b, c = executor.wrap { app.call(env) }
+ [a, b, Rack::BodyProxy.new(c) { }]
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index 922cb59280..66b4c3f1ff 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -151,7 +151,7 @@ class CounterCacheTest < ActiveRecord::TestCase
test "reset the right counter if two have the same foreign key" do
michael = people(:michael)
- assert_nothing_raised(ActiveRecord::StatementInvalid) do
+ assert_nothing_raised do
Person.reset_counters(michael.id, :friends_too)
end
end
diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb
index ba085991e0..3169408ac0 100644
--- a/activerecord/test/cases/database_statements_test.rb
+++ b/activerecord/test/cases/database_statements_test.rb
@@ -13,6 +13,12 @@ class DatabaseStatementsTest < ActiveRecord::TestCase
assert_not_nil return_the_inserted_id(method: :create)
end
+ def test_insert_update_delete_sql_is_deprecated
+ assert_deprecated { @connection.insert_sql("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") }
+ assert_deprecated { @connection.update_sql("UPDATE accounts SET credit_limit = 6000 WHERE firm_id = 42") }
+ assert_deprecated { @connection.delete_sql("DELETE FROM accounts WHERE firm_id = 42") }
+ end
+
private
def return_the_inserted_id(method:)
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index e996d142a2..f8664d83bd 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -1,7 +1,7 @@
require 'cases/helper'
require 'support/schema_dumping_helper'
-if ActiveRecord::Base.connection.supports_datetime_with_precision?
+if subsecond_precision_supported?
class DateTimePrecisionTest < ActiveRecord::TestCase
include SchemaDumpingHelper
self.use_transactional_tests = false
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index cd1967c373..a3f8d26100 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -37,8 +37,8 @@ class DirtyTest < ActiveRecord::TestCase
def test_attribute_changes
# New record - no changes.
pirate = Pirate.new
- assert !pirate.catchphrase_changed?
- assert_nil pirate.catchphrase_change
+ assert_equal false, pirate.catchphrase_changed?
+ assert_equal false, pirate.non_validated_parrot_id_changed?
# Change catchphrase.
pirate.catchphrase = 'arrr'
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 75a74c052d..f03df2d99e 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -43,7 +43,7 @@ class FinderTest < ActiveRecord::TestCase
end
assert_equal "should happen", exception.message
- assert_nothing_raised(RuntimeError) do
+ assert_nothing_raised do
Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title }
end
end
@@ -101,7 +101,7 @@ class FinderTest < ActiveRecord::TestCase
def test_find_with_ids_where_and_limit
# Please note that Topic 1 is the only not approved so
- # if it were among the first 3 it would raise a ActiveRecord::RecordNotFound
+ # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound
records = Topic.where(approved: true).limit(3).find([3,2,5,1,4])
assert_equal 3, records.size
assert_equal 'The Third Topic of the day', records[0].title
@@ -486,6 +486,66 @@ class FinderTest < ActiveRecord::TestCase
end
end
+ def test_second_to_last
+ assert_equal topics(:fourth).title, Topic.second_to_last.title
+
+ # test with offset
+ assert_equal topics(:fourth), Topic.offset(1).second_to_last
+ assert_equal topics(:fourth), Topic.offset(2).second_to_last
+ assert_equal topics(:fourth), Topic.offset(3).second_to_last
+ assert_equal nil, Topic.offset(4).second_to_last
+ assert_equal nil, Topic.offset(5).second_to_last
+
+ #test with limit
+ # assert_equal nil, Topic.limit(1).second # TODO: currently failing
+ assert_equal nil, Topic.limit(1).second_to_last
+ end
+
+ def test_second_to_last_have_primary_key_order_by_default
+ expected = topics(:fourth)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.second_to_last
+ end
+
+ def test_model_class_responds_to_second_to_last_bang
+ assert Topic.second_to_last!
+ Topic.delete_all
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
+ Topic.second_to_last!
+ end
+ end
+
+ def test_third_to_last
+ assert_equal topics(:third).title, Topic.third_to_last.title
+
+ # test with offset
+ assert_equal topics(:third), Topic.offset(1).third_to_last
+ assert_equal topics(:third), Topic.offset(2).third_to_last
+ assert_equal nil, Topic.offset(3).third_to_last
+ assert_equal nil, Topic.offset(4).third_to_last
+ assert_equal nil, Topic.offset(5).third_to_last
+
+ # test with limit
+ # assert_equal nil, Topic.limit(1).third # TODO: currently failing
+ assert_equal nil, Topic.limit(1).third_to_last
+ # assert_equal nil, Topic.limit(2).third # TODO: currently failing
+ assert_equal nil, Topic.limit(2).third_to_last
+ end
+
+ def test_third_to_last_have_primary_key_order_by_default
+ expected = topics(:third)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.third_to_last
+ end
+
+ def test_model_class_responds_to_third_to_last_bang
+ assert Topic.third_to_last!
+ Topic.delete_all
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
+ Topic.third_to_last!
+ end
+ end
+
def test_last_bang_present
assert_nothing_raised do
assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").last!
@@ -516,16 +576,44 @@ class FinderTest < ActiveRecord::TestCase
assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2)
end
- def test_last_with_integer_and_order_should_not_use_sql_limit
- query = assert_sql { Topic.order("title").last(5).entries }
- assert_equal 1, query.length
- assert_no_match(/LIMIT/, query.first)
+ def test_last_with_integer_and_order_should_use_sql_limit
+ relation = Topic.order("title")
+ assert_queries(1) { relation.last(5) }
+ assert !relation.loaded?
+ end
+
+ def test_last_with_integer_and_reorder_should_use_sql_limit
+ relation = Topic.reorder("title")
+ assert_queries(1) { relation.last(5) }
+ assert !relation.loaded?
+ end
+
+ def test_last_on_loaded_relation_should_not_use_sql
+ relation = Topic.limit(10).load
+ assert_no_queries do
+ relation.last
+ relation.last(2)
+ end
+ end
+
+ def test_last_with_irreversible_order
+ assert_deprecated do
+ Topic.order("coalesce(author_name, title)").last
+ end
end
- def test_last_with_integer_and_reorder_should_not_use_sql_limit
- query = assert_sql { Topic.reorder("title").last(5).entries }
- assert_equal 1, query.length
- assert_no_match(/LIMIT/, query.first)
+ def test_last_on_relation_with_limit_and_offset
+ post = posts('sti_comments')
+
+ comments = post.comments.order(id: :asc)
+ assert_equal comments.limit(2).to_a.last, comments.limit(2).last
+ assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2)
+ assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3)
+
+ comments = comments.offset(1)
+ assert_equal comments.limit(2).to_a.last, comments.limit(2).last
+ assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2)
+ assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3)
end
def test_take_and_first_and_last_with_integer_should_return_an_array
@@ -564,11 +652,16 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) }
end
- def test_find_on_hash_conditions_with_explicit_table_name
+ def test_find_on_hash_conditions_with_qualified_attribute_dot_notation_string
assert Topic.where('topics.approved' => false).find(1)
assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) }
end
+ def test_find_on_hash_conditions_with_qualified_attribute_dot_notation_symbol
+ assert Topic.where('topics.approved': false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved': true).find(1) }
+ end
+
def test_find_on_hash_conditions_with_hashed_table_name
assert Topic.where(topics: { approved: false }).find(1)
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) }
@@ -1058,7 +1151,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_finder_with_offset_string
- assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a }
+ assert_nothing_raised { Topic.offset("3").to_a }
end
test "find_by with hash conditions returns the first matching record" do
diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb
index 242e7a9bec..e64b90507e 100644
--- a/activerecord/test/cases/fixture_set/file_test.rb
+++ b/activerecord/test/cases/fixture_set/file_test.rb
@@ -135,6 +135,12 @@ END
end
end
+ def test_erb_filename
+ filename = 'filename.yaml'
+ erb = File.new(filename).send(:prepare_erb, "<% Rails.env %>\n")
+ assert_equal erb.filename, filename
+ end
+
private
def tmp_yaml(name, contents)
t = Tempfile.new name
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 95f8706d73..d2fdf03e9d 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -1,5 +1,3 @@
-require File.expand_path('../../../../load_paths', __FILE__)
-
require 'config'
require 'active_support/testing/autorun'
diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb
index 5ba9a1029a..9fc75b7377 100644
--- a/activerecord/test/cases/hot_compatibility_test.rb
+++ b/activerecord/test/cases/hot_compatibility_test.rb
@@ -1,7 +1,9 @@
require 'cases/helper'
+require 'support/connection_helper'
class HotCompatibilityTest < ActiveRecord::TestCase
self.use_transactional_tests = false
+ include ConnectionHelper
setup do
@klass = Class.new(ActiveRecord::Base) do
@@ -51,4 +53,90 @@ class HotCompatibilityTest < ActiveRecord::TestCase
record.reload
assert_equal 'bar', record.foo
end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ test "cleans up after prepared statement failure in a transaction" do
+ with_two_connections do |original_connection, ddl_connection|
+ record = @klass.create! bar: 'bar'
+
+ # prepare the reload statement in a transaction
+ @klass.transaction do
+ record.reload
+ end
+
+ assert get_prepared_statement_cache(@klass.connection).any?,
+ "expected prepared statement cache to have something in it"
+
+ # add a new column
+ ddl_connection.add_column :hot_compatibilities, :baz, :string
+
+ assert_raise(ActiveRecord::PreparedStatementCacheExpired) do
+ @klass.transaction do
+ record.reload
+ end
+ end
+
+ assert_empty get_prepared_statement_cache(@klass.connection),
+ "expected prepared statement cache to be empty but it wasn't"
+ end
+ end
+
+ test "cleans up after prepared statement failure in nested transactions" do
+ with_two_connections do |original_connection, ddl_connection|
+ record = @klass.create! bar: 'bar'
+
+ # prepare the reload statement in a transaction
+ @klass.transaction do
+ record.reload
+ end
+
+ assert get_prepared_statement_cache(@klass.connection).any?,
+ "expected prepared statement cache to have something in it"
+
+ # add a new column
+ ddl_connection.add_column :hot_compatibilities, :baz, :string
+
+ assert_raise(ActiveRecord::PreparedStatementCacheExpired) do
+ @klass.transaction do
+ @klass.transaction do
+ @klass.transaction do
+ record.reload
+ end
+ end
+ end
+ end
+
+ assert_empty get_prepared_statement_cache(@klass.connection),
+ "expected prepared statement cache to be empty but it wasn't"
+ end
+ end
+ end
+
+ private
+
+ def get_prepared_statement_cache(connection)
+ connection.instance_variable_get(:@statements)
+ .instance_variable_get(:@cache)[Process.pid]
+ end
+
+ # Rails will automatically clear the prepared statements on the connection
+ # that runs the migration, so we use two connections to simulate what would
+ # actually happen on a production system; we'd have one connection running the
+ # migration from the rake task ("ddl_connection" here), and we'd have another
+ # connection in a web worker.
+ def with_two_connections
+ run_without_connection do |original_connection|
+ ActiveRecord::Base.establish_connection(original_connection.merge(pool_size: 2))
+ begin
+ ddl_connection = ActiveRecord::Base.connection_pool.checkout
+ begin
+ yield original_connection, ddl_connection
+ ensure
+ ActiveRecord::Base.connection_pool.checkin ddl_connection
+ end
+ ensure
+ ActiveRecord::Base.clear_all_connections!
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 7da6842047..e234b9a6a9 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -441,12 +441,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
include InheritanceTestHelper
fixtures :companies
- def setup
- ActiveSupport::Dependencies.log_activity = true
- end
-
teardown do
- ActiveSupport::Dependencies.log_activity = false
self.class.const_remove :FirmOnTheFly rescue nil
Firm.const_remove :FirmOnTheFly rescue nil
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 4fe76e563a..6c59d7337a 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -441,7 +441,7 @@ unless in_memory_db?
def test_lock_sending_custom_lock_statement
Person.transaction do
person = Person.find(1)
- assert_sql(/LIMIT \$\d FOR SHARE NOWAIT/) do
+ assert_sql(/LIMIT \$?\d FOR SHARE NOWAIT/) do
person.lock!('FOR SHARE NOWAIT')
end
end
diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
index 6d5b6243db..60ca90464d 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -21,7 +21,7 @@ module ActiveRecord
teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Migration.verbose = @verbose_was
- ActiveRecord::SchemaMigration.delete_all
+ ActiveRecord::SchemaMigration.delete_all rescue nil
end
def test_migration_doesnt_remove_named_index
@@ -101,6 +101,18 @@ module ActiveRecord
assert connection.columns(:testings).find { |c| c.name == 'created_at' }.null
assert connection.columns(:testings).find { |c| c.name == 'updated_at' }.null
end
+
+ def test_legacy_migrations_get_deprecation_warning_when_run
+ migration = Class.new(ActiveRecord::Migration) {
+ def up
+ add_column :testings, :baz, :string
+ end
+ }
+
+ assert_deprecated do
+ migration.migrate :up
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb
index b01415afb2..9e19eb9f73 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -144,6 +144,52 @@ module ActiveRecord
@connection.drop_table "testing", if_exists: true
end
end
+
+ class CreateDogsMigration < ActiveRecord::Migration::Current
+ def change
+ create_table :dog_owners
+
+ create_table :dogs do |t|
+ t.references :dog_owner, foreign_key: true
+ end
+ end
+ end
+
+ def test_references_foreign_key_with_prefix
+ ActiveRecord::Base.table_name_prefix = 'p_'
+ migration = CreateDogsMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("p_dogs").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_prefix = nil
+ end
+
+ def test_references_foreign_key_with_suffix
+ ActiveRecord::Base.table_name_suffix = '_s'
+ migration = CreateDogsMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("dogs_s").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ ActiveRecord::Base.table_name_suffix = nil
+ end
+
+ test "multiple foreign keys can be added to the same table" do
+ @connection.create_table :testings do |t|
+ t.integer :col_1
+ t.integer :col_2
+
+ t.foreign_key :testing_parents, column: :col_1
+ t.foreign_key :testing_parents, column: :col_2
+ end
+
+ fks = @connection.foreign_keys("testings")
+
+ fk_definitions = fks.map {|fk| [fk.from_table, fk.to_table, fk.column] }
+ assert_equal([["testings", "testing_parents", "col_1"],
+ ["testings", "testing_parents", "col_2"]], fk_definitions)
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index b9ce6bbc55..70c64f3e71 100644
--- a/activerecord/test/cases/migration/references_statements_test.rb
+++ b/activerecord/test/cases/migration/references_statements_test.rb
@@ -55,6 +55,11 @@ module ActiveRecord
assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id')
end
+ def test_creates_named_unique_index
+ add_reference table_name, :tag, index: { name: 'index_taggings_on_tag_id', unique: true }
+ assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id', unique: true )
+ end
+
def test_creates_reference_id_with_specified_type
add_reference table_name, :user, type: :string
assert column_exists?(table_name, :user_id, :string)
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index bae0467e72..5a6d2ce80c 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -516,13 +516,12 @@ class MigrationTest < ActiveRecord::TestCase
data_column = columns.detect { |c| c.name == "data" }
assert_nil data_column.default
-
+ ensure
Person.connection.drop_table :binary_testings, if_exists: true
end
unless mysql_enforcing_gtid_consistency?
def test_create_table_with_query
- Person.connection.drop_table :table_from_query_testings rescue nil
Person.connection.create_table(:person, force: true)
Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person"
@@ -530,12 +529,11 @@ class MigrationTest < ActiveRecord::TestCase
columns = Person.connection.columns(:table_from_query_testings)
assert_equal 1, columns.length
assert_equal "id", columns.first.name
-
+ ensure
Person.connection.drop_table :table_from_query_testings rescue nil
end
def test_create_table_with_query_from_relation
- Person.connection.drop_table :table_from_query_testings rescue nil
Person.connection.create_table(:person, force: true)
Person.connection.create_table :table_from_query_testings, as: Person.select(:id)
@@ -543,7 +541,7 @@ class MigrationTest < ActiveRecord::TestCase
columns = Person.connection.columns(:table_from_query_testings)
assert_equal 1, columns.length
assert_equal "id", columns.first.name
-
+ ensure
Person.connection.drop_table :table_from_query_testings rescue nil
end
end
@@ -586,8 +584,7 @@ class MigrationTest < ActiveRecord::TestCase
end
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
- def test_out_of_range_limit_should_raise
- Person.connection.drop_table :test_limits rescue nil
+ def test_out_of_range_integer_limit_should_raise
e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
Person.connection.create_table :test_integer_limits, :force => true do |t|
t.column :bigone, :integer, :limit => 10
@@ -595,16 +592,22 @@ class MigrationTest < ActiveRecord::TestCase
end
assert_match(/No integer type has byte size 10/, e.message)
+ ensure
+ Person.connection.drop_table :test_integer_limits, if_exists: true
+ end
+ end
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
- Person.connection.create_table :test_text_limits, :force => true do |t|
- t.column :bigtext, :text, :limit => 0xfffffffff
- end
+ if current_adapter?(:Mysql2Adapter)
+ def test_out_of_range_text_limit_should_raise
+ e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
+ Person.connection.create_table :test_text_limits, force: true do |t|
+ t.text :bigtext, limit: 0xfffffffff
end
end
- Person.connection.drop_table :test_limits rescue nil
+ assert_match(/No text type has byte length #{0xfffffffff}/, e.message)
+ ensure
+ Person.connection.drop_table :test_text_limits, if_exists: true
end
end
@@ -726,7 +729,7 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase
connection.add_index :values, :value
connection.remove_index :values, :column => :value
end
-
+ ensure
connection.drop_table :values rescue nil
end
end
@@ -738,11 +741,11 @@ class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase
t.integer :value
end
- assert_nothing_raised ArgumentError do
+ assert_nothing_raised do
connection.add_index :values, :value, name: 'a_different_name'
connection.remove_index :values, column: :value, name: 'a_different_name'
end
-
+ ensure
connection.drop_table :values rescue nil
end
end
@@ -1104,4 +1107,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
ActiveRecord::Base.logger = old
end
+ def test_unknown_migration_version_should_raise_an_argument_error
+ assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] }
+ end
end
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index 7f31325f47..486bcc22df 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -63,7 +63,7 @@ class ModulesTest < ActiveRecord::TestCase
def test_assign_ids
firm = MyApplication::Business::Firm.first
- assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do
+ assert_nothing_raised do
firm.client_ids = [MyApplication::Business::Client.first.id]
end
end
@@ -72,7 +72,7 @@ class ModulesTest < ActiveRecord::TestCase
def test_eager_loading_in_modules
clients = []
- assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do
+ assert_nothing_raised do
clients << MyApplication::Business::Client.references(:accounts).merge!(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3)
clients << MyApplication::Business::Client.includes(:firm => :account).find(3)
end
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 39cdcf5403..af4183a601 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -104,7 +104,7 @@ class MultipleDbTest < ActiveRecord::TestCase
def test_associations_should_work_when_model_has_no_connection
begin
ActiveRecord::Base.remove_connection
- assert_nothing_raised ActiveRecord::ConnectionNotEstablished do
+ assert_nothing_raised do
College.first.courses.first
end
ensure
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 6fbc6196cc..11fb164d50 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -61,6 +61,13 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
assert_equal "No association found for name `honesty'. Has it been defined yet?", exception.message
end
+ def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes
+ exception = assert_raise ActiveModel::UnknownAttributeError do
+ Pirate.new(:ship_attributes => { :sail => true })
+ end
+ assert_equal "unknown attribute 'sail' for Ship.", exception.message
+ end
+
def test_should_disable_allow_destroy_by_default
Pirate.accepts_nested_attributes_for :ship
@@ -69,7 +76,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
pirate.update(ship_attributes: { '_destroy' => true, :id => ship.id })
- assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.ship.reload }
+ assert_nothing_raised { pirate.ship.reload }
end
def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction
@@ -173,7 +180,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
pirate.ship_attributes = { :id => "" }
- assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.save! }
+ assert_nothing_raised { pirate.save! }
end
def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record
@@ -504,7 +511,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
[nil, '0', 0, 'false', false].each do |not_truth|
@ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: not_truth })
- assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
+ assert_nothing_raised { @ship.pirate.reload }
end
end
@@ -512,7 +519,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc(&:empty?)
@ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: '1' })
- assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
+ assert_nothing_raised { @ship.pirate.reload }
ensure
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?)
end
@@ -529,7 +536,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
pirate = @ship.pirate
@ship.attributes = { :pirate_attributes => { :id => pirate.id, '_destroy' => true } }
- assert_nothing_raised(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) }
+ assert_nothing_raised { Pirate.find(pirate.id) }
@ship.save
assert_raise(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) }
end
@@ -582,6 +589,13 @@ module NestedAttributesOnACollectionAssociationTests
assert_respond_to @pirate, association_setter
end
+ def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes_for_has_many
+ exception = assert_raise ActiveModel::UnknownAttributeError do
+ @pirate.parrots_attributes = [{ peg_leg: true }]
+ end
+ assert_equal "unknown attribute 'peg_leg' for Parrot.", exception.message
+ end
+
def test_should_save_only_one_association_on_create
pirate = Pirate.create!({
:catchphrase => 'Arr',
@@ -699,7 +713,7 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_should_not_assign_destroy_key_to_a_record
- assert_nothing_raised ActiveRecord::UnknownAttributeError do
+ assert_nothing_raised do
@pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }})
end
end
@@ -734,8 +748,8 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed
- assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) }
- assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) }
+ assert_nothing_raised { @pirate.send(association_setter, {}) }
+ assert_nothing_raised { @pirate.send(association_setter, Hash.new) }
exception = assert_raise ArgumentError do
@pirate.send(association_setter, "foo")
@@ -810,7 +824,7 @@ module NestedAttributesOnACollectionAssociationTests
def test_can_use_symbols_as_object_identifier
@pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } }
- assert_nothing_raised(NoMethodError) { @pirate.save! }
+ assert_nothing_raised { @pirate.save! }
end
def test_numeric_column_changes_from_zero_to_no_empty_string
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index b918b36b94..52eac4a124 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -130,7 +130,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_supports_primary_key
- assert_nothing_raised NoMethodError do
+ assert_nothing_raised do
ActiveRecord::Base.connection.supports_primary_key?
end
end
@@ -228,9 +228,10 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
def test_any_type_primary_key
assert_equal "code", Barcode.primary_key
- column_type = Barcode.type_for_attribute(Barcode.primary_key)
- assert_equal :string, column_type.type
- assert_equal 42, column_type.limit
+ column = Barcode.column_for_attribute(Barcode.primary_key)
+ assert_not column.null
+ assert_equal :string, column.type
+ assert_equal 42, column.limit
end
test "schema dump primary key includes type and options" do
@@ -350,9 +351,9 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
test "schema dump primary key with bigserial" do
schema = dump_table_schema "widgets"
if current_adapter?(:PostgreSQLAdapter)
- assert_match %r{create_table "widgets", id: :bigserial}, schema
+ assert_match %r{create_table "widgets", id: :bigserial, force: :cascade}, schema
else
- assert_match %r{create_table "widgets", id: :bigint}, schema
+ assert_match %r{create_table "widgets", id: :bigint, force: :cascade}, schema
end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index d84653e4c9..e53239cdee 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -16,7 +16,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_exceptional_middleware_clears_and_disables_cache_on_error
assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
Task.find 1
Task.find 1
assert_equal 1, ActiveRecord::Base.connection.query_cache.length
@@ -31,7 +31,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_exceptional_middleware_leaves_enabled_cache_alone
ActiveRecord::Base.connection.enable_query_cache!
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
raise "lol borked"
}
assert_raises(RuntimeError) { mw.call({}) }
@@ -42,7 +42,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_exceptional_middleware_assigns_original_connection_id_on_error
connection_id = ActiveRecord::Base.connection_id
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
ActiveRecord::Base.connection_id = self.object_id
raise "lol borked"
}
@@ -53,7 +53,7 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_middleware_delegates
called = false
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
called = true
[200, {}, nil]
}
@@ -62,7 +62,7 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def test_middleware_caches
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
Task.find 1
Task.find 1
assert_equal 1, ActiveRecord::Base.connection.query_cache.length
@@ -74,50 +74,13 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_cache_enabled_during_call
assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off'
- mw = ActiveRecord::QueryCache.new lambda { |env|
+ mw = middleware { |env|
assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on'
[200, {}, nil]
}
mw.call({})
end
- def test_cache_on_during_body_write
- streaming = Class.new do
- def each
- yield ActiveRecord::Base.connection.query_cache_enabled
- end
- end
-
- mw = ActiveRecord::QueryCache.new lambda { |env|
- [200, {}, streaming.new]
- }
- body = mw.call({}).last
- body.each { |x| assert x, 'cache should be on' }
- body.close
- assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled'
- end
-
- def test_cache_off_after_close
- mw = ActiveRecord::QueryCache.new lambda { |env| [200, {}, nil] }
- body = mw.call({}).last
-
- assert ActiveRecord::Base.connection.query_cache_enabled, 'cache enabled'
- body.close
- assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled'
- end
-
- def test_cache_clear_after_close
- mw = ActiveRecord::QueryCache.new lambda { |env|
- Post.first
- [200, {}, nil]
- }
- body = mw.call({}).last
-
- assert !ActiveRecord::Base.connection.query_cache.empty?, 'cache not empty'
- body.close
- assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty'
- end
-
def test_cache_passing_a_relation
post = Post.first
Post.cache do
@@ -244,6 +207,13 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_equal 0, Post.where(title: 'rollback').to_a.count
end
end
+
+ private
+ def middleware(&app)
+ executor = Class.new(ActiveSupport::Executor)
+ ActiveRecord::QueryCache.install_executor_hooks executor
+ lambda { |env| executor.wrap { app.call(env) } }
+ end
end
class QueryCacheExpiryTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb
index 28a0862f91..ce8c5ca489 100644
--- a/activerecord/test/cases/relation/or_test.rb
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -82,5 +82,11 @@ module ActiveRecord
assert_equal p.loaded?, true
assert_equal expected, p.or(Post.where('id = 2')).to_a
end
+
+ def test_or_with_non_relation_object_raises_error
+ assert_raises ArgumentError do
+ Post.where(id: [1, 2, 3]).or(title: 'Rails')
+ end
+ end
end
end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index bc6378b90e..56a2b5b8c6 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -2,6 +2,7 @@ require "cases/helper"
require "models/author"
require "models/binary"
require "models/cake_designer"
+require "models/car"
require "models/chef"
require "models/comment"
require "models/edge"
@@ -14,7 +15,7 @@ require "models/vertex"
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges, :authors, :binaries, :essays
+ fixtures :posts, :edges, :authors, :binaries, :essays, :cars, :treasures, :price_estimates
def test_where_copies_bind_params
author = authors(:david)
@@ -114,6 +115,17 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_polymorphic_array_where_multiple_types
+ treasure_1 = treasures(:diamond)
+ treasure_2 = treasures(:sapphire)
+ car = cars(:honda)
+
+ expected = [price_estimates(:diamond), price_estimates(:sapphire_1), price_estimates(:sapphire_2), price_estimates(:honda)].sort
+ actual = PriceEstimate.where(estimate_of: [treasure_1, treasure_2, car]).to_a.sort
+
+ assert_equal expected, actual
+ end
+
def test_polymorphic_nested_relation_where
expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2]))
actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2]))
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 090b885dd5..95e4230a58 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -358,6 +358,12 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_sanitized_order
query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql
assert_match(/field\(id, 1,3,2\)/, query)
+
+ query = Tag.order(["field(id, ?)", []]).to_sql
+ assert_match(/field\(id, NULL\)/, query)
+
+ query = Tag.order(["field(id, ?)", nil]).to_sql
+ assert_match(/field\(id, NULL\)/, query)
end
def test_finding_with_order_limit_and_offset
@@ -1273,6 +1279,16 @@ class RelationTest < ActiveRecord::TestCase
assert posts.loaded?
end
+ def test_to_a_should_dup_target
+ posts = Post.all
+
+ original_size = posts.size
+ removed = posts.to_a.pop
+
+ assert_equal original_size, posts.size
+ assert_includes posts.to_a, removed
+ end
+
def test_build
posts = Post.all
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 8def74e75b..4498c96029 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -147,10 +147,10 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{c_int_7.*limit: 7}, output
assert_match %r{c_int_8.*limit: 8}, output
else
- assert_match %r{c_int_5.*limit: 8}, output
- assert_match %r{c_int_6.*limit: 8}, output
- assert_match %r{c_int_7.*limit: 8}, output
- assert_match %r{c_int_8.*limit: 8}, output
+ assert_match %r{t\.bigint\s+"c_int_5"$}, output
+ assert_match %r{t\.bigint\s+"c_int_6"$}, output
+ assert_match %r{t\.bigint\s+"c_int_7"$}, output
+ assert_match %r{t\.bigint\s+"c_int_8"$}, output
end
end
@@ -248,12 +248,12 @@ class SchemaDumperTest < ActiveRecord::TestCase
if current_adapter?(:PostgreSQLAdapter)
def test_schema_dump_includes_bigint_default
output = standard_dump
- assert_match %r{t\.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
+ assert_match %r{t\.bigint\s+"bigint_default",\s+default: 0}, output
end
def test_schema_dump_includes_limit_on_array_type
output = standard_dump
- assert_match %r{t\.integer\s+"big_int_data_points\",\s+limit: 8,\s+array: true}, output
+ assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output
end
def test_schema_dump_allows_array_of_decimal_defaults
@@ -323,9 +323,9 @@ class SchemaDumperTest < ActiveRecord::TestCase
create_table("dogs") do |t|
t.column :name, :string
t.column :owner_id, :integer
+ t.index [:name]
+ t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys?
end
- add_index "dogs", [:name]
- add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys?
end
def down
drop_table("dogs")
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index c918cbdef5..dcd09b6973 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -4,6 +4,7 @@ require 'models/comment'
require 'models/developer'
require 'models/computer'
require 'models/vehicle'
+require 'models/cat'
class DefaultScopingTest < ActiveRecord::TestCase
fixtures :developers, :posts, :comments
@@ -485,4 +486,15 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal 1, SubConditionalStiPost.all.to_a.size
assert_equal 2, SubConditionalStiPost.unscope(where: :title).to_a.size
end
+
+ def test_with_abstract_class_scope_should_be_executed_in_correct_context
+ vegetarian_pattern, gender_pattern = if current_adapter?(:Mysql2Adapter)
+ [/`lions`.`is_vegetarian`/, /`lions`.`gender`/]
+ else
+ [/"lions"."is_vegetarian"/, /"lions"."gender"/]
+ end
+
+ assert_match vegetarian_pattern, Lion.all.to_sql
+ assert_match gender_pattern, Lion.female.to_sql
+ end
end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index acba97bbb8..96c94eefa0 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -544,4 +544,24 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal 1, SpecialComment.where(body: 'go crazy').created.count
end
+ def test_model_class_should_respond_to_empty
+ assert !Topic.empty?
+ Topic.delete_all
+ assert Topic.empty?
+ end
+
+ def test_model_class_should_respond_to_none
+ assert !Topic.none?
+ Topic.delete_all
+ assert Topic.none?
+ end
+
+ def test_model_class_should_respond_to_one
+ assert !Topic.one?
+ Topic.delete_all
+ assert !Topic.one?
+ Topic.create!
+ assert Topic.one?
+ end
+
end
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index a704b861cb..104226010a 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -94,5 +94,17 @@ module ActiveRecord
additional_books = cache.execute([], Book, Book.connection)
assert first_books != additional_books
end
+
+ def test_unprepared_statements_dont_share_a_cache_with_prepared_statements
+ Book.create(name: "my book")
+ Book.create(name: "my other book")
+
+ book = Book.find_by(name: "my book")
+ other_book = Book.connection.unprepared_statement do
+ Book.find_by(name: "my other book")
+ end
+
+ refute_equal book, other_book
+ end
end
end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index ab63f5825c..bce86875e1 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -177,6 +177,7 @@ class StoreTest < ActiveRecord::TestCase
assert_equal [:color], first_model.stored_attributes[:data]
assert_equal [:color, :width, :height], second_model.stored_attributes[:data]
assert_equal [:color, :area, :volume], third_model.stored_attributes[:data]
+ assert_equal [:color], first_model.stored_attributes[:data]
end
test "YAML coder initializes the store when a Nil value is given" do
diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb
index 72c5c16555..7d44e36419 100644
--- a/activerecord/test/cases/suppressor_test.rb
+++ b/activerecord/test/cases/suppressor_test.rb
@@ -46,7 +46,18 @@ class SuppressorTest < ActiveRecord::TestCase
Notification.suppress { UserWithNotification.create! }
assert_difference -> { Notification.count } do
- Notification.create!
+ Notification.create!(message: "New Comment")
+ end
+ end
+
+ def test_suppresses_validations_on_create
+ assert_no_difference -> { Notification.count } do
+ Notification.suppress do
+ User.create
+ User.create!
+ User.new.save
+ User.new.save!
+ end
end
end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 49df6628eb..e6d731e1e1 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -8,6 +8,13 @@ module ActiveRecord
ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks
ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks
ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
end
end
@@ -161,21 +168,25 @@ module ActiveRecord
with('database' => 'dev-db')
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with('database' => 'test-db')
- ENV.expects(:[]).with('RAILS_ENV').returns(nil)
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new('development')
)
end
- def test_creates_only_development_database_when_rails_env_is_development
+ def test_creates_test_and_development_databases_when_rails_env_is_development
+ old_env = ENV['RAILS_ENV']
+ ENV['RAILS_ENV'] = 'development'
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with('database' => 'dev-db')
- ENV.expects(:[]).with('RAILS_ENV').returns('development')
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with('database' => 'test-db')
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new('development')
)
+ ensure
+ ENV['RAILS_ENV'] = old_env
end
def test_establishes_connection_for_the_given_environment
@@ -282,38 +293,53 @@ module ActiveRecord
with('database' => 'dev-db')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'test-db')
- ENV.expects(:[]).with('RAILS_ENV').returns(nil)
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new('development')
)
end
- def test_drops_only_development_database_when_rails_env_is_development
+ def test_drops_testand_development_databases_when_rails_env_is_development
+ old_env = ENV['RAILS_ENV']
+ ENV['RAILS_ENV'] = 'development'
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'dev-db')
- ENV.expects(:[]).with('RAILS_ENV').returns('development')
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with('database' => 'test-db')
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new('development')
)
+ ensure
+ ENV['RAILS_ENV'] = old_env
end
end
class DatabaseTasksMigrateTest < ActiveRecord::TestCase
+ def setup
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path'
+ end
+
+ def teardown
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil
+ end
+
def test_migrate_receives_correct_env_vars
verbose, version = ENV['VERBOSE'], ENV['VERSION']
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path'
ENV['VERBOSE'] = 'false'
ENV['VERSION'] = '4'
ActiveRecord::Migrator.expects(:migrate).with('custom/path', 4)
ActiveRecord::Tasks::DatabaseTasks.migrate
ensure
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil
ENV['VERBOSE'], ENV['VERSION'] = verbose, version
end
+
+ def test_migrate_clears_schema_cache_afterward
+ ActiveRecord::Base.expects(:clear_cache!)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
end
class DatabaseTasksPurgeTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 1632f04854..8e480bbaee 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'active_record/tasks/database_tasks'
if current_adapter?(:Mysql2Adapter)
module ActiveRecord
@@ -12,6 +13,13 @@ module ActiveRecord
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
end
def test_establishes_connection_without_database
@@ -48,14 +56,20 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
- def test_create_when_database_exists_outputs_info_to_stderr
- $stderr.expects(:puts).with("my-app-db already exists").once
+ def test_when_database_created_successfully_outputs_info_to_stdout
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ assert_equal $stdout.string, "Created database 'my-app-db'\n"
+ end
+
+ def test_create_when_database_exists_outputs_info_to_stderr
ActiveRecord::Base.connection.stubs(:create_database).raises(
- ActiveRecord::StatementInvalid.new("Can't create database 'dev'; database exists:")
+ ActiveRecord::Tasks::DatabaseAlreadyExists
)
ActiveRecord::Tasks::DatabaseTasks.create @configuration
+
+ assert_equal $stderr.string, "Database 'my-app-db' already exists\n"
end
end
@@ -77,6 +91,13 @@ module ActiveRecord
ActiveRecord::Base.stubs(:establish_connection).
raises(@error).
then.returns(true)
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
end
def test_root_password_is_requested
@@ -160,6 +181,13 @@ module ActiveRecord
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
end
def test_establishes_connection_to_mysql_database
@@ -173,6 +201,12 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop @configuration
end
+
+ def test_when_database_dropped_successfully_outputs_info_to_stdout
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+
+ assert_equal $stdout.string, "Dropped database 'my-app-db'\n"
+ end
end
class MySQLPurgeTest < ActiveRecord::TestCase
@@ -307,6 +341,5 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
end
-
end
end
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index ba53f340ae..6a0c7fbcb5 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'active_record/tasks/database_tasks'
if current_adapter?(:PostgreSQLAdapter)
module ActiveRecord
@@ -12,6 +13,13 @@ module ActiveRecord
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
end
def test_establishes_connection_to_postgresql_database
@@ -63,14 +71,20 @@ module ActiveRecord
assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration }
end
- def test_create_when_database_exists_outputs_info_to_stderr
- $stderr.expects(:puts).with("my-app-db already exists").once
+ def test_when_database_created_successfully_outputs_info_to_stdout
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ assert_equal $stdout.string, "Created database 'my-app-db'\n"
+ end
+
+ def test_create_when_database_exists_outputs_info_to_stderr
ActiveRecord::Base.connection.stubs(:create_database).raises(
- ActiveRecord::StatementInvalid.new('database "my-app-db" already exists')
+ ActiveRecord::Tasks::DatabaseAlreadyExists
)
ActiveRecord::Tasks::DatabaseTasks.create @configuration
+
+ assert_equal $stderr.string, "Database 'my-app-db' already exists\n"
end
end
@@ -84,6 +98,13 @@ module ActiveRecord
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
end
def test_establishes_connection_to_postgresql_database
@@ -101,6 +122,12 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop @configuration
end
+
+ def test_when_database_dropped_successfully_outputs_info_to_stdout
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+
+ assert_equal $stdout.string, "Dropped database 'my-app-db'\n"
+ end
end
class PostgreSQLPurgeTest < ActiveRecord::TestCase
@@ -273,6 +300,5 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
end
-
end
end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index 0aea0c3b38..4be03c7f61 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'active_record/tasks/database_tasks'
require 'pathname'
if current_adapter?(:SQLite3Adapter)
@@ -15,6 +16,13 @@ module ActiveRecord
File.stubs(:exist?).returns(false)
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
end
def test_db_checks_database_exists
@@ -23,12 +31,18 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
end
+ def test_when_db_created_successfully_outputs_info_to_stdout
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
+
+ assert_equal $stdout.string, "Created database '#{@database}'\n"
+ end
+
def test_db_create_when_file_exists
File.stubs(:exist?).returns(true)
- $stderr.expects(:puts).with("#{@database} already exists")
-
ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root'
+
+ assert_equal $stderr.string, "Database '#{@database}' already exists\n"
end
def test_db_create_with_file_does_nothing
@@ -69,6 +83,13 @@ module ActiveRecord
Pathname.stubs(:new).returns(@path)
File.stubs(:join).returns('/former/relative/path')
FileUtils.stubs(:rm).returns(true)
+
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
+ end
+
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
end
def test_creates_path_from_database
@@ -103,6 +124,12 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root'
end
+
+ def test_when_db_dropped_successfully_outputs_info_to_stdout
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root'
+
+ assert_equal $stdout.string, "Dropped database '#{@database}'\n"
+ end
end
class SqliteDBCharsetTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index 3b6e4dcc2b..628a8eb771 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -1,7 +1,7 @@
require 'cases/helper'
require 'support/schema_dumping_helper'
-if ActiveRecord::Base.connection.supports_datetime_with_precision?
+if subsecond_precision_supported?
class TimePrecisionTest < ActiveRecord::TestCase
include SchemaDumpingHelper
self.use_transactional_tests = false
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 970f6bcf4a..937b84bccc 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -98,8 +98,11 @@ class TimestampTest < ActiveRecord::TestCase
task = Task.first
previous_value = task.ending
task.touch(:ending)
+
+ now = Time.now.change(usec: 0)
+
assert_not_equal previous_value, task.ending
- assert_in_delta Time.now, task.ending, 1
+ assert_in_delta now, task.ending, 1
end
def test_touching_an_attribute_updates_timestamp_with_given_time
@@ -120,10 +123,12 @@ class TimestampTest < ActiveRecord::TestCase
previous_ending = task.ending
task.touch(:starting, :ending)
+ now = Time.now.change(usec: 0)
+
assert_not_equal previous_starting, task.starting
assert_not_equal previous_ending, task.ending
- assert_in_delta Time.now, task.starting, 1
- assert_in_delta Time.now, task.ending, 1
+ assert_in_delta now, task.starting, 1
+ assert_in_delta now, task.ending, 1
end
def test_touching_a_record_without_timestamps_is_unexceptional
diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb
index dd43ee358c..c0b3750bcc 100644
--- a/activerecord/test/cases/validations/absence_validation_test.rb
+++ b/activerecord/test/cases/validations/absence_validation_test.rb
@@ -57,19 +57,17 @@ class AbsenceValidationTest < ActiveRecord::TestCase
assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? }
end
- def test_does_not_validate_if_parent_record_is_validate_false
+ def test_validates_absence_of_virtual_attribute_on_model
repair_validations(Interest) do
- Interest.validates_absence_of(:topic)
- interest = Interest.new(topic: Topic.new(title: "Math"))
- interest.save!(validate: false)
- assert interest.persisted?
+ Interest.send(:attr_accessor, :token)
+ Interest.validates_absence_of(:token)
- man = Man.new(interest_ids: [interest.id])
- man.save!
-
- assert_equal man.interests.size, 1
+ interest = Interest.create!(topic: 'Thought Leadering')
assert interest.valid?
- assert man.valid?
+
+ interest.token = 'tl'
+
+ assert interest.invalid?
end
end
end
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index 981239c4d6..b8307d6665 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -47,8 +47,6 @@ class I18nValidationTest < ActiveRecord::TestCase
# [ "given on condition", {on: :save}, {}]
]
- # validates_uniqueness_of w/ mocha
-
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_uniqueness_of on generated message #{name}" do
Topic.validates_uniqueness_of :title, validation_options
@@ -59,8 +57,6 @@ class I18nValidationTest < ActiveRecord::TestCase
end
end
- # validates_associated w/ mocha
-
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_associated on generated message #{name}" do
Topic.validates_associated :replies, validation_options
@@ -70,8 +66,6 @@ class I18nValidationTest < ActiveRecord::TestCase
end
end
- # validates_associated w/o mocha
-
def test_validates_associated_finds_custom_model_key_translation
I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}}
I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}}
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
index c5d8f8895c..78263fd955 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -61,17 +61,19 @@ class LengthValidationTest < ActiveRecord::TestCase
assert_equal pet_count, Pet.count
end
- def test_does_not_validate_length_of_if_parent_record_is_validate_false
- @owner.validates_length_of :name, minimum: 1
- owner = @owner.new
- owner.save!(validate: false)
- assert owner.persisted?
+ def test_validates_length_of_virtual_attribute_on_model
+ repair_validations(Pet) do
+ Pet.send(:attr_accessor, :nickname)
+ Pet.validates_length_of(:name, minimum: 1)
+ Pet.validates_length_of(:nickname, minimum: 1)
- pet = Pet.new(owner_id: owner.id)
- pet.save!
+ pet = Pet.create!(name: 'Fancy Pants', nickname: 'Fancy')
- assert_equal owner.pets.size, 1
- assert owner.valid?
- assert pet.valid?
+ assert pet.valid?
+
+ pet.nickname = ''
+
+ assert pet.invalid?
+ end
end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 6f8ad06ab6..868d111b8c 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -65,19 +65,39 @@ class PresenceValidationTest < ActiveRecord::TestCase
assert_nothing_raised { s.valid? }
end
- def test_does_not_validate_presence_of_if_parent_record_is_validate_false
+ def test_validates_presence_of_virtual_attribute_on_model
repair_validations(Interest) do
+ Interest.send(:attr_accessor, :abbreviation)
Interest.validates_presence_of(:topic)
+ Interest.validates_presence_of(:abbreviation)
+
+ interest = Interest.create!(topic: 'Thought Leadering', abbreviation: 'tl')
+ assert interest.valid?
+
+ interest.abbreviation = ''
+
+ assert interest.invalid?
+ end
+ end
+
+ def test_validations_run_on_persisted_record
+ repair_validations(Interest) do
interest = Interest.new
- interest.save!(validate: false)
- assert interest.persisted?
+ interest.save!
+ assert_predicate interest, :valid?
- man = Man.new(interest_ids: [interest.id])
- man.save!
+ Interest.validates_presence_of(:topic)
- assert_equal man.interests.size, 1
- assert interest.valid?
- assert man.valid?
+ assert_not_predicate interest, :valid?
+ end
+ end
+
+ def test_validates_presence_with_on_context
+ repair_validations(Interest) do
+ Interest.validates_presence_of(:topic, on: :required_name)
+ interest = Interest.new
+ interest.save!
+ assert_not interest.valid?(:required_name)
end
end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 6264ec15d0..4b0a590adb 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -5,6 +5,7 @@ require 'models/warehouse_thing'
require 'models/guid'
require 'models/event'
require 'models/dashboard'
+require 'models/uuid_item'
class Wizard < ActiveRecord::Base
self.abstract_class = true
@@ -48,6 +49,18 @@ class BigIntReverseTest < ActiveRecord::Base
validates :engines_count, uniqueness: true
end
+class CoolTopic < Topic
+ validates_uniqueness_of :id
+end
+
+class TopicWithAfterCreate < Topic
+ after_create :set_author
+
+ def set_author
+ update_attributes!(:author_name => "#{title} #{id}")
+ end
+end
+
class UniquenessValidationTest < ActiveRecord::TestCase
INT_MAX_VALUE = 2147483647
@@ -337,12 +350,17 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_with_limit
if current_adapter?(:SQLite3Adapter)
- # Event.title has limit 5, but does not affected in SQLite.
+ # Event.title has limit 5, but SQLite doesn't truncate.
e1 = Event.create(title: "abcdefgh")
assert e1.valid?, "Could not create an event with a unique 8 characters title"
+
e2 = Event.create(title: "abcdefgh")
- assert !e2.valid?, "Created an event whose title is not unique"
- elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter)
+ assert_not e2.valid?, "Created an event whose title is not unique"
+ elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)
+ assert_raise(ActiveRecord::ValueTooLong) do
+ Event.create(title: "abcdefgh")
+ end
+ else
assert_raise(ActiveRecord::StatementInvalid) do
Event.create(title: "abcdefgh")
end
@@ -351,12 +369,17 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_with_limit_and_utf8
if current_adapter?(:SQLite3Adapter)
- # Event.title has limit 5, but does not affected in SQLite.
+ # Event.title has limit 5, but does SQLite doesn't truncate.
e1 = Event.create(title: "一二三四五六七八")
assert e1.valid?, "Could not create an event with a unique 8 characters title"
+
e2 = Event.create(title: "一二三四五六七八")
- assert !e2.valid?, "Created an event whose title is not unique"
- elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter)
+ assert_not e2.valid?, "Created an event whose title is not unique"
+ elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)
+ assert_raise(ActiveRecord::ValueTooLong) do
+ Event.create(title: "一二三四五六七八")
+ end
+ else
assert_raise(ActiveRecord::StatementInvalid) do
Event.create(title: "一二三四五六七八")
end
@@ -424,23 +447,6 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert topic.valid?
end
- def test_does_not_validate_uniqueness_of_if_parent_record_is_validate_false
- Reply.validates_uniqueness_of(:content)
-
- Reply.create!(content: "Topic Title")
-
- reply = Reply.new(content: "Topic Title")
- reply.save!(validate: false)
- assert reply.persisted?
-
- topic = Topic.new(reply_ids: [reply.id])
- topic.save!
-
- assert_equal topic.replies.size, 1
- assert reply.valid?
- assert topic.valid?
- end
-
def test_validate_uniqueness_of_custom_primary_key
klass = Class.new(ActiveRecord::Base) do
self.table_name = "keyboards"
@@ -492,4 +498,35 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t.valid?, "Should be valid"
assert t.save, "Should still save t as unique"
end
+
+ def test_validate_uniqueness_with_after_create_performing_save
+ TopicWithAfterCreate.validates_uniqueness_of(:title)
+ topic = TopicWithAfterCreate.create!(:title => "Title1")
+ assert topic.author_name.start_with?("Title1")
+
+ topic2 = TopicWithAfterCreate.new(:title => "Title1")
+ refute topic2.valid?
+ assert_equal(["has already been taken"], topic2.errors[:title])
+ end
+
+ def test_validate_uniqueness_uuid
+ skip unless current_adapter?(:PostgreSQLAdapter)
+ item = UuidItem.create!(uuid: SecureRandom.uuid, title: 'item1')
+ item.update(title: 'item1-title2')
+ assert_empty item.errors
+
+ item2 = UuidValidatingItem.create!(uuid: SecureRandom.uuid, title: 'item2')
+ item2.update(title: 'item2-title2')
+ assert_empty item2.errors
+ end
+
+ def test_validate_uniqueness_regular_id
+ item = CoolTopic.create!(title: 'MyItem')
+ assert_empty item.errors
+
+ item2 = CoolTopic.new(id: item.id, title: 'MyItem2')
+ refute item2.valid?
+
+ assert_equal(["has already been taken"], item2.errors[:id])
+ end
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index d04f4f7ce7..85e33d2218 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -130,7 +130,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_validates_acceptance_of_with_non_existent_table
Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base)
- assert_nothing_raised ActiveRecord::StatementInvalid do
+ assert_nothing_raised do
IncorporealModel.validates_acceptance_of(:incorporeal_column)
end
end
diff --git a/activerecord/test/fixtures/price_estimates.yml b/activerecord/test/fixtures/price_estimates.yml
index 1149ab17a2..406d65a142 100644
--- a/activerecord/test/fixtures/price_estimates.yml
+++ b/activerecord/test/fixtures/price_estimates.yml
@@ -1,7 +1,16 @@
-saphire_1:
+sapphire_1:
price: 10
estimate_of: sapphire (Treasure)
sapphire_2:
price: 20
estimate_of: sapphire (Treasure)
+
+diamond:
+ price: 30
+ estimate_of: diamond (Treasure)
+
+honda:
+ price: 40
+ estimate_of_type: Car
+ estimate_of_id: 1
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index f25e31b13d..38b983eda0 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -144,6 +144,14 @@ class Author < ActiveRecord::Base
has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
+ has_many :posts_with_extension, -> { order(:title) }, class_name: "Post" do
+ def extension_method; end
+ end
+
+ has_many :posts_with_extension_and_instance, ->(record) { order(:title) }, class_name: "Post" do
+ def extension_method; end
+ end
+
attr_accessor :post_log
after_initialize :set_post_log
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index 778c22b1f6..0f37e9a289 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -12,6 +12,8 @@ class Car < ActiveRecord::Base
has_many :engines, :dependent => :destroy, inverse_of: :my_car
has_many :wheels, :as => :wheelable, :dependent => :destroy
+ has_many :price_estimates, :as => :estimate_of
+
scope :incl_tyres, -> { includes(:tyres) }
scope :incl_engines, -> { includes(:engines) }
diff --git a/activerecord/test/models/cat.rb b/activerecord/test/models/cat.rb
new file mode 100644
index 0000000000..dfdde18641
--- /dev/null
+++ b/activerecord/test/models/cat.rb
@@ -0,0 +1,10 @@
+class Cat < ActiveRecord::Base
+ self.abstract_class = true
+
+ enum gender: [:female, :male]
+
+ default_scope -> { where(is_vegetarian: false) }
+end
+
+class Lion < Cat
+end
diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb
index b4b4b8f1b6..82edc64b68 100644
--- a/activerecord/test/models/notification.rb
+++ b/activerecord/test/models/notification.rb
@@ -1,2 +1,3 @@
class Notification < ActiveRecord::Base
+ validates_presence_of :message
end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index cedb774b10..ce8242cf2f 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,7 +1,8 @@
class Owner < ActiveRecord::Base
self.primary_key = :owner_id
has_many :pets, -> { order 'pets.name desc' }
- has_many :toys, :through => :pets
+ has_many :toys, through: :pets
+ has_many :persons, through: :pets
belongs_to :last_pet, class_name: 'Pet'
scope :including_last_pet, -> {
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index f7970d7aab..53489fa1b3 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -4,6 +4,9 @@ class Pet < ActiveRecord::Base
self.primary_key = :pet_id
belongs_to :owner, :touch => true
has_many :toys
+ has_many :pet_treasures
+ has_many :treasures, through: :pet_treasures
+ has_many :persons, through: :treasures, source: :looter, source_type: 'Person'
class << self
attr_accessor :after_destroy_output
diff --git a/activerecord/test/models/pet_treasure.rb b/activerecord/test/models/pet_treasure.rb
new file mode 100644
index 0000000000..1fe7807ffe
--- /dev/null
+++ b/activerecord/test/models/pet_treasure.rb
@@ -0,0 +1,6 @@
+class PetTreasure < ActiveRecord::Base
+ self.table_name = "pets_treasures"
+
+ belongs_to :pet
+ belongs_to :treasure
+end
diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb
index 80d4725f7e..b48b9a2155 100644
--- a/activerecord/test/models/tag.rb
+++ b/activerecord/test/models/tag.rb
@@ -5,3 +5,9 @@ class Tag < ActiveRecord::Base
has_many :tagged_posts, :through => :taggings, :source => 'taggable', :source_type => 'Post'
end
+
+class OrderedTag < Tag
+ self.table_name = "tags"
+
+ has_many :taggings, -> { order('taggings.id DESC') }, foreign_key: 'tag_id'
+end
diff --git a/activerecord/test/models/uuid_item.rb b/activerecord/test/models/uuid_item.rb
new file mode 100644
index 0000000000..2353e40213
--- /dev/null
+++ b/activerecord/test/models/uuid_item.rb
@@ -0,0 +1,6 @@
+class UuidItem < ActiveRecord::Base
+end
+
+class UuidValidatingItem < UuidItem
+ validates_uniqueness_of :uuid
+end
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index 101e657982..5a49b38457 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -33,6 +33,7 @@ ActiveRecord::Schema.define do
create_table :collation_tests, id: false, force: true do |t|
t.string :string_cs_column, limit: 1, collation: 'utf8_bin'
t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci'
+ t.binary :binary_column, limit: 1
end
ActiveRecord::Base.connection.execute <<-SQL
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index 3a5d73a0ed..24713f722a 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -106,4 +106,9 @@ _SQL
t.integer :big_int_data_points, limit: 8, array: true
t.decimal :decimal_array_default, array: true, default: [1.23, 3.45]
end
+
+ create_table :uuid_items, force: true, id: false do |t|
+ t.uuid :uuid, primary_key: true
+ t.string :title
+ end
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index b9e0706d60..1027bcb365 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,10 +1,4 @@
ActiveRecord::Schema.define do
- def except(adapter_names_to_exclude)
- unless [adapter_names_to_exclude].flatten.include?(adapter_name)
- yield
- end
- end
-
# ------------------------------------------------------------------- #
# #
# Please keep these create table statements in alphabetical order #
@@ -421,6 +415,11 @@ ActiveRecord::Schema.define do
t.integer :amount
end
+ create_table :lions, force: true do |t|
+ t.integer :gender
+ t.boolean :is_vegetarian, default: false
+ end
+
create_table :lock_without_defaults, force: true do |t|
t.column :lock_version, :integer
end
@@ -615,6 +614,12 @@ ActiveRecord::Schema.define do
end
end
+ create_table :pets_treasures, force: true do |t|
+ t.column :treasure_id, :integer
+ t.column :pet_id, :integer
+ t.column :rainbow_color, :string
+ end
+
create_table :pirates, force: true do |t|
t.column :catchphrase, :string
t.column :parrot_id, :integer
@@ -929,7 +934,7 @@ ActiveRecord::Schema.define do
t.string :treaty_id
t.string :name
end
- create_table :countries_treaties, force: true, id: false do |t|
+ create_table :countries_treaties, force: true, primary_key: [:country_id, :treaty_id] do |t|
t.string :country_id, null: false
t.string :treaty_id, null: false
end
@@ -986,7 +991,7 @@ ActiveRecord::Schema.define do
create_table :records, force: true do |t|
end
- except 'SQLite' do
+ if supports_foreign_keys?
# fk_test_has_fk should be before fk_test_has_pk
create_table :fk_test_has_fk, force: true do |t|
t.integer :fk_id, null: false
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 3d4cc8fae6..16b0a06d44 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,13 +1,130 @@
+* `ActiveSupport::Duration` supports ISO8601 formatting and parsing.
+
+ ActiveSupport::Duration.parse('P3Y6M4DT12H30M5S')
+ # => 3 years, 6 months, 4 days, 12 hours, 30 minutes, and 5 seconds
+
+ (3.years + 3.days).iso8601
+ # => "P3Y3D"
+
+ Inspired by Arnau Siches' [ISO8601 gem](https://github.com/arnau/ISO8601/)
+ and rewritten by Andrey Novikov with suggestions from Andrew White. Test
+ data from the ISO8601 gem redistributed under MIT license.
+
+ (Will be used to support the PostgreSQL interval data type.)
+
+ *Andrey Novikov*, *Arnau Siches*, *Andrew White*
+
+* `Cache#fetch(key, force: true)` forces a cache miss, so it must be called
+ with a block to provide a new value to cache. Fetching with `force: true`
+ but without a block now raises ArgumentError.
+
+ cache.fetch('key', force: true) # => ArgumentError
+
+ *Santosh Wadghule*
+
+* `ActiveSupport::Duration` supports weeks and hours.
+
+ [1.hour.inspect, 1.hour.value, 1.hour.parts]
+ # => ["3600 seconds", 3600, [[:seconds, 3600]]] # Before
+ # => ["1 hour", 3600, [[:hours, 1]]] # After
+
+ [1.week.inspect, 1.week.value, 1.week.parts]
+ # => ["7 days", 604800, [[:days, 7]]] # Before
+ # => ["1 week", 604800, [[:weeks, 1]]] # After
+
+ This brings us into closer conformance with ISO8601 and relieves some
+ astonishment about getting `1.hour.inspect # => 3600 seconds`.
+
+ Compatibility: The duration's `value` remains the same, so apps using
+ durations are oblivious to the new time periods. Apps, libraries, and
+ plugins that work with the internal `parts` hash will need to broaden
+ their time period handling to cover hours & weeks.
+
+ *Andrey Novikov*
+
+* Fix behavior of JSON encoding for `Exception`.
+
+ *namusyaka*
+
+* Make `number_to_phone` format number with regexp pattern.
+
+ number_to_phone(18812345678, pattern: /(\d{3})(\d{4})(\d{4})/)
+ # => 188-1234-5678
+
+ *Pan Gaoyong*
+
+* Match `String#to_time`'s behaviour to that of ruby's implementation for edge cases.
+
+ `nil` is now returned instead of the current date if the string provided does
+ contain time information, but none that is used to build the `Time` object.
+
+ Fixes #22958.
+
+ *Siim Liiser*
+
+* Rely on the native DateTime#<=> implementation to handle non-datetime like
+ objects instead of returning `nil` ourselves. This restores the ability
+ of `DateTime` instances to be compared with a `Numeric` that represents an
+ astronomical julian day number.
+
+ Fixes #24228.
+
+ *Andrew White*
+
+* Add `String#upcase_first` method.
+
+ *Glauco Custódio*, *bogdanvlviv*
+
+* Prevent `Marshal.load` from looping infinitely when trying to autoload a constant
+ which resolves to a different name.
+
+ *Olek Janiszewski*
+
+* Deprecate `Module.local_constants`. Please use `Module.constants(false)` instead.
+
+ *Yuichiro Kaneko*
+
+* Publish `ActiveSupport::Executor` and `ActiveSupport::Reloader` APIs to allow
+ components and libraries to manage, and participate in, the execution of
+ application code, and the application reloading process.
+
+ *Matthew Draper*
+
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* Deprecate arguments on `assert_nothing_raised`.
+
+ `assert_nothing_raised` does not assert the arguments that have been passed
+ in (usually a specific exception class) since the method only yields the
+ block. So as not to confuse the users that the arguments have meaning, they
+ are being deprecated.
+
+ *Tara Scherner de la Fuente*
+
+* Make `benchmark('something', silence: true)` actually work.
+
+ *DHH*
+
+* Add `#on_weekday?` method to `Date`, `Time`, and `DateTime`.
+
+ `#on_weekday?` returns `true` if the receiving date/time does not fall on a Saturday
+ or Sunday.
+
+ *Vipul A M*
+
* Add `Array#second_to_last` and `Array#third_to_last` methods.
*Brian Christian*
-* Fix regression in `Hash#dig` for HashWithIndifferentAccess.
- *Jon Moss*
+* Fix regression in `Hash#dig` for HashWithIndifferentAccess.
+
+ *Jon Moss*
+
## Rails 5.0.0.beta2 (February 01, 2016) ##
-* Change number_to_currency behavior for checking negativity.
+* Change `number_to_currency` behavior for checking negativity.
Used `to_f.negative` instead of using `to_f.phase` for checking negativity
of a number in number_to_currency helper.
@@ -42,6 +159,7 @@
*Akshay Vishnoi*
+
## Rails 5.0.0.beta1 (December 18, 2015) ##
* Add thread_m/cattr_accessor/reader/writer suite of methods for declaring class and module variables that live per-thread.
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index 68a80701ed..71fe4d7253 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.homepage = 'http://rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']
s.require_path = 'lib'
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 94fe893149..72777baecd 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -33,10 +33,13 @@ module ActiveSupport
autoload :Concern
autoload :Dependencies
autoload :DescendantsTracker
+ autoload :ExecutionWrapper
+ autoload :Executor
autoload :FileUpdateChecker
autoload :EventedFileUpdateChecker
autoload :LogSubscriber
autoload :Notifications
+ autoload :Reloader
eager_autoload do
autoload :BacktraceCleaner
diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb
index 805b7a714f..3988b147ac 100644
--- a/activesupport/lib/active_support/benchmarkable.rb
+++ b/activesupport/lib/active_support/benchmarkable.rb
@@ -38,7 +38,7 @@ module ActiveSupport
options[:level] ||= :info
result = nil
- ms = Benchmark.ms { result = options[:silence] ? silence { yield } : yield }
+ ms = Benchmark.ms { result = options[:silence] ? logger.silence { yield } : yield }
logger.send(options[:level], '%s (%.1fms)' % [ message, ms ])
result
else
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 610105f41c..bc114e0785 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -158,20 +158,20 @@ module ActiveSupport
attr_reader :silence, :options
alias :silence? :silence
- # Create a new cache. The options will be passed to any write method calls
+ # Creates a new cache. The options will be passed to any write method calls
# except for <tt>:namespace</tt> which can be used to set the global
# namespace for the cache.
def initialize(options = nil)
@options = options ? options.dup : {}
end
- # Silence the logger.
+ # Silences the logger.
def silence!
@silence = true
self
end
- # Silence the logger within a block.
+ # Silences the logger within a block.
def mute
previous_silence, @silence = defined?(@silence) && @silence, true
yield
@@ -198,10 +198,17 @@ module ActiveSupport
# cache.fetch('city') # => "Duckburgh"
#
# You may also specify additional options via the +options+ argument.
- # Setting <tt>force: true</tt> will force a cache miss:
+ # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
+ # the cache value as missing even if it's present. Passing a block is
+ # required when `force` is true so this always results in a cache write.
#
# cache.write('today', 'Monday')
- # cache.fetch('today', force: true) # => nil
+ # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
+ # cache.fetch('today', force: true) # => ArgumentError
+ #
+ # The `:force` option is useful when you're calling some other method to
+ # ask whether you should force a cache write. Otherwise, it's clearer to
+ # just call `Cache#write`.
#
# Setting <tt>:compress</tt> will store a large cache entry set by the call
# in a compressed format.
@@ -292,6 +299,8 @@ module ActiveSupport
else
save_block_result_to_cache(name, options) { |_name| yield _name }
end
+ elsif options && options[:force]
+ raise ArgumentError, 'Missing block: Calling `Cache#fetch` with `force: true` requires a block.'
else
read(name, options)
end
@@ -323,7 +332,7 @@ module ActiveSupport
end
end
- # Read multiple values at once from the cache. Options can be passed
+ # Reads multiple values at once from the cache. Options can be passed
# in the last argument.
#
# Some cache implementation may optimize this method.
@@ -333,21 +342,19 @@ module ActiveSupport
options = names.extract_options!
options = merged_options(options)
- instrument_multi(:read, names, options) do |payload|
- results = {}
- names.each do |name|
- key = normalize_key(name, options)
- entry = read_entry(key, options)
- if entry
- if entry.expired?
- delete_entry(key, options)
- else
- results[name] = entry.value
- end
+ results = {}
+ names.each do |name|
+ key = normalize_key(name, options)
+ entry = read_entry(key, options)
+ if entry
+ if entry.expired?
+ delete_entry(key, options)
+ else
+ results[name] = entry.value
end
end
- results
end
+ results
end
# Fetches data from the cache, using the given keys. If there is data in
@@ -415,7 +422,7 @@ module ActiveSupport
end
end
- # Delete all entries with keys matching the pattern.
+ # Deletes all entries with keys matching the pattern.
#
# Options are passed to the underlying cache implementation.
#
@@ -424,7 +431,7 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
end
- # Increment an integer value in the cache.
+ # Increments an integer value in the cache.
#
# Options are passed to the underlying cache implementation.
#
@@ -433,7 +440,7 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support increment")
end
- # Decrement an integer value in the cache.
+ # Decrements an integer value in the cache.
#
# Options are passed to the underlying cache implementation.
#
@@ -442,7 +449,7 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support decrement")
end
- # Cleanup the cache by removing expired entries.
+ # Cleanups the cache by removing expired entries.
#
# Options are passed to the underlying cache implementation.
#
@@ -451,7 +458,7 @@ module ActiveSupport
raise NotImplementedError.new("#{self.class.name} does not support cleanup")
end
- # Clear the entire cache. Be careful with this method since it could
+ # Clears the entire cache. Be careful with this method since it could
# affect other processes if shared cache is being used.
#
# The options hash is passed to the underlying cache implementation.
@@ -462,7 +469,7 @@ module ActiveSupport
end
protected
- # Add the namespace defined in the options to a pattern designed to
+ # Adds the namespace defined in the options to a pattern designed to
# match keys. Implementations that support delete_matched should call
# this method to translate a pattern that matches names into one that
# matches namespaced keys.
@@ -481,26 +488,26 @@ module ActiveSupport
end
end
- # Read an entry from the cache implementation. Subclasses must implement
+ # Reads an entry from the cache implementation. Subclasses must implement
# this method.
def read_entry(key, options) # :nodoc:
raise NotImplementedError.new
end
- # Write an entry to the cache implementation. Subclasses must implement
+ # Writes an entry to the cache implementation. Subclasses must implement
# this method.
def write_entry(key, entry, options) # :nodoc:
raise NotImplementedError.new
end
- # Delete an entry from the cache implementation. Subclasses must
+ # Deletes an entry from the cache implementation. Subclasses must
# implement this method.
def delete_entry(key, options) # :nodoc:
raise NotImplementedError.new
end
private
- # Merge the default options with ones specific to a method call.
+ # Merges the default options with ones specific to a method call.
def merged_options(call_options) # :nodoc:
if call_options
options.merge(call_options)
@@ -509,7 +516,7 @@ module ActiveSupport
end
end
- # Expand key to be a consistent string value. Invoke +cache_key+ if
+ # Expands key to be a consistent string value. Invokes +cache_key+ if
# object responds to +cache_key+. Otherwise, +to_param+ method will be
# called. If the key is a Hash, then keys will be sorted alphabetically.
def expanded_key(key) # :nodoc:
@@ -529,7 +536,7 @@ module ActiveSupport
key.to_param
end
- # Prefix a key with the namespace. Namespace and key will be delimited
+ # Prefixes a key with the namespace. Namespace and key will be delimited
# with a colon.
def normalize_key(key, options)
key = expanded_key(key)
@@ -555,17 +562,6 @@ module ActiveSupport
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
end
- def instrument_multi(operation, keys, options = nil)
- log do
- formatted_keys = keys.map { |k| "- #{k}" }.join("\n")
- "Caches multi #{operation}:\n#{formatted_keys}#{options.blank? ? "" : " (#{options.inspect})"}"
- end
-
- payload = { key: keys }
- payload.merge!(options) if options.is_a?(Hash)
- ActiveSupport::Notifications.instrument("cache_#{operation}_multi.active_support", payload) { yield(payload) }
- end
-
def log
return unless logger && logger.debug? && !silence?
logger.debug(yield)
@@ -588,12 +584,12 @@ module ActiveSupport
end
def get_entry_value(entry, name, options)
- instrument(:fetch_hit, name, options) { |payload| }
+ instrument(:fetch_hit, name, options) { }
entry.value
end
def save_block_result_to_cache(name, options)
- result = instrument(:generate, name, options) do |payload|
+ result = instrument(:generate, name, options) do
yield(name)
end
@@ -611,7 +607,7 @@ module ActiveSupport
class Entry # :nodoc:
DEFAULT_COMPRESS_LIMIT = 16.kilobytes
- # Create a new cache entry for the specified value. Options supported are
+ # Creates a new cache entry for the specified value. Options supported are
# +:compress+, +:compress_threshold+, and +:expires_in+.
def initialize(value, options = {})
if should_compress?(value, options)
@@ -630,7 +626,7 @@ module ActiveSupport
compressed? ? uncompress(@value) : @value
end
- # Check if the entry is expired. The +expires_in+ parameter can override
+ # Checks if the entry is expired. The +expires_in+ parameter can override
# the value set when the entry was created.
def expired?
@expires_in && @created_at + @expires_in <= Time.now.to_f
@@ -665,7 +661,7 @@ module ActiveSupport
end
end
- # Duplicate the value in a class. This is used by cache implementations that don't natively
+ # Duplicates the value in a class. This is used by cache implementations that don't natively
# serialize entries to protect against accidental cache modifications.
def dup_value!
if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 174913365a..2ca4b51efa 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -96,16 +96,14 @@ module ActiveSupport
options = names.extract_options!
options = merged_options(options)
- instrument_multi(:read, names, options) do
- keys_to_names = Hash[names.map{|name| [normalize_key(name, options), name]}]
- raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
- values = {}
- raw_values.each do |key, value|
- entry = deserialize_entry(value)
- values[keys_to_names[key]] = entry.value unless entry.expired?
- end
- values
+ keys_to_names = Hash[names.map{|name| [normalize_key(name, options), name]}]
+ raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
+ values = {}
+ raw_values.each do |key, value|
+ entry = deserialize_entry(value)
+ values[keys_to_names[key]] = entry.value unless entry.expired?
end
+ values
end
# Increment a cached value. This method uses the memcached incr atomic
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index df38dbcf11..1c678dc2af 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -126,7 +126,7 @@ module ActiveSupport
def set_cache_value(value, name, amount, options) # :nodoc:
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
`set_cache_value` is deprecated and will be removed from Rails 5.1.
- Please use `write_cache_value`
+ Please use `write_cache_value` instead.
MESSAGE
write_cache_value name, value, options
end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index e6baddf5db..904d3f0eb0 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -571,15 +571,15 @@ module ActiveSupport
# Install a callback for the given event.
#
- # set_callback :save, :before, :before_meth
- # set_callback :save, :after, :after_meth, if: :condition
+ # set_callback :save, :before, :before_method
+ # set_callback :save, :after, :after_method, if: :condition
# set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
#
# The second argument indicates whether the callback is to be run +:before+,
# +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
# means the first example above can also be written as:
#
- # set_callback :save, :before_meth
+ # set_callback :save, :before_method
#
# The callback can be specified as a symbol naming an instance method; as a
# proc, lambda, or block; as a string to be instance evaluated(deprecated); or as an
@@ -782,7 +782,7 @@ module ActiveSupport
def display_deprecation_warning_for_false_terminator
ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails.
+ Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in Rails 5.1.
To explicitly halt the callback chain, please use `throw :abort` instead.
MSG
end
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index e079af594d..4da7fdd159 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/object/try'
+
module DateAndTime
module Calculations
DAYS_INTO_WEEK = {
@@ -51,6 +53,11 @@ module DateAndTime
WEEKEND_DAYS.include?(wday)
end
+ # Returns true if the date/time does not fall on a Saturday or Sunday.
+ def on_weekday?
+ !WEEKEND_DAYS.include?(wday)
+ end
+
# Returns a new date/time the specified number of days ago.
def days_ago(days)
advance(:days => -days)
diff --git a/activesupport/lib/active_support/core_ext/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb
index bcb228b09a..5450533935 100644
--- a/activesupport/lib/active_support/core_ext/date_time.rb
+++ b/activesupport/lib/active_support/core_ext/date_time.rb
@@ -2,4 +2,3 @@ require 'active_support/core_ext/date_time/acts_like'
require 'active_support/core_ext/date_time/blank'
require 'active_support/core_ext/date_time/calculations'
require 'active_support/core_ext/date_time/conversions'
-require 'active_support/core_ext/date_time/zones'
diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index 95617fb8c2..ac46f5ffe8 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -165,13 +165,10 @@ class DateTime
# Layers additional behavior on DateTime#<=> so that Time and
# ActiveSupport::TimeWithZone instances can be compared with a DateTime.
def <=>(other)
- if other.kind_of?(Infinity)
- super
- elsif other.respond_to? :to_datetime
+ if other.respond_to? :to_datetime
super other.to_datetime rescue nil
else
- nil
+ super
end
end
-
end
diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb
deleted file mode 100644
index c39f358395..0000000000
--- a/activesupport/lib/active_support/core_ext/date_time/zones.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-require 'date'
-require 'active_support/core_ext/date_and_time/zones'
-
-class DateTime
- include DateAndTime::Zones
-end
diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb
index 8b2366c4b3..1bfa18aeee 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -11,7 +11,7 @@ class Hash
# hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"}
def transform_keys
return enum_for(:transform_keys) { size } unless block_given?
- result = self.class.new
+ result = {}
each_key do |key|
result[yield(key)] = self[key]
end
diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb
index e333b26133..edfc8296fe 100644
--- a/activesupport/lib/active_support/core_ext/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -3,9 +3,12 @@ module ActiveSupport
def load(source)
super(source)
rescue ArgumentError, NameError => exc
- if exc.message.match(%r|undefined class/module (.+)|)
+ if exc.message.match(%r|undefined class/module (.+?)(?:::)?\z|)
# try loading the class/module
- $1.constantize
+ loaded = $1.constantize
+
+ raise unless $1 == loaded.name
+
# if it is an IO we need to go back to read the object
source.rewind if source.respond_to?(:rewind)
retry
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
index 76825862d7..567ac825e9 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -27,7 +27,7 @@ class Module
# <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
#
# module HairColors
- # mattr_writer :hair_colors, instance_reader: false
+ # mattr_reader :hair_colors, instance_reader: false
# end
#
# class Person
@@ -40,7 +40,7 @@ class Module
# Also, you can pass a block to set up the attribute with a default value.
#
# module HairColors
- # cattr_reader :hair_colors do
+ # mattr_reader :hair_colors do
# [:brown, :black, :blonde, :red]
# end
# end
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
index 8a7e6776da..0b3d18301e 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
@@ -47,7 +47,7 @@ class Module
unless options[:instance_reader] == false || options[:instance_accessor] == false
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}
- Thread.current[:"attr_#{self.class.name}_#{sym}"]
+ Thread.current[:"attr_#{name}_#{sym}"]
end
EOS
end
@@ -86,7 +86,7 @@ class Module
unless options[:instance_writer] == false || options[:instance_accessor] == false
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
def #{sym}=(obj)
- Thread.current[:"attr_#{self.class.name}_#{sym}"] = obj
+ Thread.current[:"attr_#{name}_#{sym}"] = obj
end
EOS
end
diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb
index f1d26ef28f..fa692e1b0e 100644
--- a/activesupport/lib/active_support/core_ext/module/introspection.rb
+++ b/activesupport/lib/active_support/core_ext/module/introspection.rb
@@ -57,6 +57,10 @@ class Module
end
def local_constants #:nodoc:
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Module#local_constants is deprecated and will be removed in Rails 5.1.
+ Use Module#constants(false) instead.
+ MSG
constants(false)
end
end
diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
index 9d832897ed..b25925b9d4 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -15,72 +15,72 @@ module ActiveSupport::NumericWithFormat
# ==== Examples
#
# Phone Numbers:
- # 5551234.to_s(:phone) # => 555-1234
- # 1235551234.to_s(:phone) # => 123-555-1234
- # 1235551234.to_s(:phone, area_code: true) # => (123) 555-1234
- # 1235551234.to_s(:phone, delimiter: ' ') # => 123 555 1234
- # 1235551234.to_s(:phone, area_code: true, extension: 555) # => (123) 555-1234 x 555
- # 1235551234.to_s(:phone, country_code: 1) # => +1-123-555-1234
+ # 5551234.to_s(:phone) # => "555-1234"
+ # 1235551234.to_s(:phone) # => "123-555-1234"
+ # 1235551234.to_s(:phone, area_code: true) # => "(123) 555-1234"
+ # 1235551234.to_s(:phone, delimiter: ' ') # => "123 555 1234"
+ # 1235551234.to_s(:phone, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
+ # 1235551234.to_s(:phone, country_code: 1) # => "+1-123-555-1234"
# 1235551234.to_s(:phone, country_code: 1, extension: 1343, delimiter: '.')
- # # => +1.123.555.1234 x 1343
+ # # => "+1.123.555.1234 x 1343"
#
# Currency:
- # 1234567890.50.to_s(:currency) # => $1,234,567,890.50
- # 1234567890.506.to_s(:currency) # => $1,234,567,890.51
- # 1234567890.506.to_s(:currency, precision: 3) # => $1,234,567,890.506
- # 1234567890.506.to_s(:currency, locale: :fr) # => 1 234 567 890,51 €
+ # 1234567890.50.to_s(:currency) # => "$1,234,567,890.50"
+ # 1234567890.506.to_s(:currency) # => "$1,234,567,890.51"
+ # 1234567890.506.to_s(:currency, precision: 3) # => "$1,234,567,890.506"
+ # 1234567890.506.to_s(:currency, locale: :fr) # => "1 234 567 890,51 €"
# -1234567890.50.to_s(:currency, negative_format: '(%u%n)')
- # # => ($1,234,567,890.50)
+ # # => "($1,234,567,890.50)"
# 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '')
- # # => &pound;1234567890,50
+ # # => "&pound;1234567890,50"
# 1234567890.50.to_s(:currency, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
- # # => 1234567890,50 &pound;
+ # # => "1234567890,50 &pound;"
#
# Percentage:
- # 100.to_s(:percentage) # => 100.000%
- # 100.to_s(:percentage, precision: 0) # => 100%
- # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => 1.000,000%
- # 302.24398923423.to_s(:percentage, precision: 5) # => 302.24399%
- # 1000.to_s(:percentage, locale: :fr) # => 1 000,000%
- # 100.to_s(:percentage, format: '%n %') # => 100.000 %
+ # 100.to_s(:percentage) # => "100.000%"
+ # 100.to_s(:percentage, precision: 0) # => "100%"
+ # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => "1.000,000%"
+ # 302.24398923423.to_s(:percentage, precision: 5) # => "302.24399%"
+ # 1000.to_s(:percentage, locale: :fr) # => "1 000,000%"
+ # 100.to_s(:percentage, format: '%n %') # => "100.000 %"
#
# Delimited:
- # 12345678.to_s(:delimited) # => 12,345,678
- # 12345678.05.to_s(:delimited) # => 12,345,678.05
- # 12345678.to_s(:delimited, delimiter: '.') # => 12.345.678
- # 12345678.to_s(:delimited, delimiter: ',') # => 12,345,678
- # 12345678.05.to_s(:delimited, separator: ' ') # => 12,345,678 05
- # 12345678.05.to_s(:delimited, locale: :fr) # => 12 345 678,05
+ # 12345678.to_s(:delimited) # => "12,345,678"
+ # 12345678.05.to_s(:delimited) # => "12,345,678.05"
+ # 12345678.to_s(:delimited, delimiter: '.') # => "12.345.678"
+ # 12345678.to_s(:delimited, delimiter: ',') # => "12,345,678"
+ # 12345678.05.to_s(:delimited, separator: ' ') # => "12,345,678 05"
+ # 12345678.05.to_s(:delimited, locale: :fr) # => "12 345 678,05"
# 98765432.98.to_s(:delimited, delimiter: ' ', separator: ',')
- # # => 98 765 432,98
+ # # => "98 765 432,98"
#
# Rounded:
- # 111.2345.to_s(:rounded) # => 111.235
- # 111.2345.to_s(:rounded, precision: 2) # => 111.23
- # 13.to_s(:rounded, precision: 5) # => 13.00000
- # 389.32314.to_s(:rounded, precision: 0) # => 389
- # 111.2345.to_s(:rounded, significant: true) # => 111
- # 111.2345.to_s(:rounded, precision: 1, significant: true) # => 100
- # 13.to_s(:rounded, precision: 5, significant: true) # => 13.000
- # 111.234.to_s(:rounded, locale: :fr) # => 111,234
+ # 111.2345.to_s(:rounded) # => "111.235"
+ # 111.2345.to_s(:rounded, precision: 2) # => "111.23"
+ # 13.to_s(:rounded, precision: 5) # => "13.00000"
+ # 389.32314.to_s(:rounded, precision: 0) # => "389"
+ # 111.2345.to_s(:rounded, significant: true) # => "111"
+ # 111.2345.to_s(:rounded, precision: 1, significant: true) # => "100"
+ # 13.to_s(:rounded, precision: 5, significant: true) # => "13.000"
+ # 111.234.to_s(:rounded, locale: :fr) # => "111,234"
# 13.to_s(:rounded, precision: 5, significant: true, strip_insignificant_zeros: true)
- # # => 13
- # 389.32314.to_s(:rounded, precision: 4, significant: true) # => 389.3
+ # # => "13"
+ # 389.32314.to_s(:rounded, precision: 4, significant: true) # => "389.3"
# 1111.2345.to_s(:rounded, precision: 2, separator: ',', delimiter: '.')
- # # => 1.111,23
+ # # => "1.111,23"
#
# Human-friendly size in Bytes:
- # 123.to_s(:human_size) # => 123 Bytes
- # 1234.to_s(:human_size) # => 1.21 KB
- # 12345.to_s(:human_size) # => 12.1 KB
- # 1234567.to_s(:human_size) # => 1.18 MB
- # 1234567890.to_s(:human_size) # => 1.15 GB
- # 1234567890123.to_s(:human_size) # => 1.12 TB
- # 1234567890123456.to_s(:human_size) # => 1.1 PB
- # 1234567890123456789.to_s(:human_size) # => 1.07 EB
- # 1234567.to_s(:human_size, precision: 2) # => 1.2 MB
- # 483989.to_s(:human_size, precision: 2) # => 470 KB
- # 1234567.to_s(:human_size, precision: 2, separator: ',') # => 1,2 MB
+ # 123.to_s(:human_size) # => "123 Bytes"
+ # 1234.to_s(:human_size) # => "1.21 KB"
+ # 12345.to_s(:human_size) # => "12.1 KB"
+ # 1234567.to_s(:human_size) # => "1.18 MB"
+ # 1234567890.to_s(:human_size) # => "1.15 GB"
+ # 1234567890123.to_s(:human_size) # => "1.12 TB"
+ # 1234567890123456.to_s(:human_size) # => "1.1 PB"
+ # 1234567890123456789.to_s(:human_size) # => "1.07 EB"
+ # 1234567.to_s(:human_size, precision: 2) # => "1.2 MB"
+ # 483989.to_s(:human_size, precision: 2) # => "470 KB"
+ # 1234567.to_s(:human_size, precision: 2, separator: ',') # => "1,2 MB"
# 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB"
# 524288000.to_s(:human_size, precision: 5) # => "500 MB"
#
diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb
index 6c4a975495..c6ece22f8d 100644
--- a/activesupport/lib/active_support/core_ext/numeric/time.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/time.rb
@@ -25,17 +25,17 @@ class Numeric
# Returns a Duration instance matching the number of minutes provided.
#
- # 2.minutes # => 120 seconds
+ # 2.minutes # => 2 minutes
def minutes
- ActiveSupport::Duration.new(self * 60, [[:seconds, self * 60]])
+ ActiveSupport::Duration.new(self * 60, [[:minutes, self]])
end
alias :minute :minutes
# Returns a Duration instance matching the number of hours provided.
#
- # 2.hours # => 7_200 seconds
+ # 2.hours # => 2 hours
def hours
- ActiveSupport::Duration.new(self * 3600, [[:seconds, self * 3600]])
+ ActiveSupport::Duration.new(self * 3600, [[:hours, self]])
end
alias :hour :hours
@@ -49,17 +49,17 @@ class Numeric
# Returns a Duration instance matching the number of weeks provided.
#
- # 2.weeks # => 14 days
+ # 2.weeks # => 2 weeks
def weeks
- ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]])
+ ActiveSupport::Duration.new(self * 7.days, [[:weeks, self]])
end
alias :week :weeks
# Returns a Duration instance matching the number of fortnights provided.
#
- # 2.fortnights # => 28 days
+ # 2.fortnights # => 4 weeks
def fortnights
- ActiveSupport::Duration.new(self * 2.weeks, [[:days, self * 14]])
+ ActiveSupport::Duration.new(self * 2.weeks, [[:weeks, self * 2]])
end
alias :fortnight :fortnights
diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb
index 0db787010c..d49b2fbe54 100644
--- a/activesupport/lib/active_support/core_ext/object/json.rb
+++ b/activesupport/lib/active_support/core_ext/object/json.rb
@@ -197,3 +197,9 @@ class Process::Status #:nodoc:
{ :exitstatus => exitstatus, :pid => pid }
end
end
+
+class Exception
+ def as_json(options = nil)
+ to_s
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index fd79a40e31..71612e09fa 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -18,7 +18,8 @@ class String
# "12/13/2012".to_time # => ArgumentError: argument out of range
def to_time(form = :local)
parts = Date._parse(self, false)
- return if parts.empty?
+ used_keys = %i(year mon mday hour min sec sec_fraction offset)
+ return if (parts.keys & used_keys).empty?
now = Time.now
time = Time.new(
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index cc71b8155d..7277f51076 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -222,6 +222,15 @@ class String
ActiveSupport::Inflector.humanize(self, options)
end
+ # Converts just the first character to uppercase.
+ #
+ # 'what a Lovely Day'.upcase_first # => "What a Lovely Day"
+ # 'w'.upcase_first # => "W"
+ # ''.upcase_first # => ""
+ def upcase_first
+ ActiveSupport::Inflector.upcase_first(self)
+ end
+
# Creates a foreign key name from a class name.
# +separate_class_name_and_id_with_underscore+ sets whether
# the method should put '_' between the name and 'id'.
diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb
index 43b9fd4bf7..005ad93b08 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -250,7 +250,7 @@ end
class String
# Marks a string as trusted safe. It will be inserted into HTML with no
- # additional escaping performed. It is your responsibilty to ensure that the
+ # additional escaping performed. It is your responsibility to ensure that the
# string contains no malicious content. This method is equivalent to the
# `raw` helper in views. It is recommended that you use `sanitize` instead of
# this method. It should never be called on user input.
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index af18ff746f..57f6286de3 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -88,15 +88,6 @@ module ActiveSupport #:nodoc:
mattr_accessor :explicitly_unloadable_constants
self.explicitly_unloadable_constants = []
- # The logger is used for generating information on the action run-time
- # (including benchmarking) if available. Can be set to nil for no logging.
- # Compatible with both Ruby's own Logger and Log4r loggers.
- mattr_accessor :logger
-
- # Set to +true+ to enable logging of const_missing and file loads.
- mattr_accessor :log_activity
- self.log_activity = false
-
# The WatchStack keeps a stack of the modules being watched as files are
# loaded. If a file in the process of being loaded (parent.rb) triggers the
# load of another file (child.rb) the stack will ensure that child.rb
@@ -143,11 +134,11 @@ module ActiveSupport #:nodoc:
next unless mod.is_a?(Module)
# Get a list of the constants that were added
- new_constants = mod.local_constants - original_constants
+ new_constants = mod.constants(false) - original_constants
- # self[namespace] returns an Array of the constants that are being evaluated
+ # @stack[namespace] returns an Array of the constants that are being evaluated
# for that namespace. For instance, if parent.rb requires child.rb, the first
- # element of self[Object] will be an Array of the constants that were present
+ # element of @stack[Object] will be an Array of the constants that were present
# before parent.rb was required. The second element will be an Array of the
# constants that were present before child.rb was required.
@stack[namespace].each do |namespace_constants|
@@ -171,7 +162,7 @@ module ActiveSupport #:nodoc:
@watching << namespaces.map do |namespace|
module_name = Dependencies.to_constant_name(namespace)
original_constants = Dependencies.qualified_const_defined?(module_name) ?
- Inflector.constantize(module_name).local_constants : []
+ Inflector.constantize(module_name).constants(false) : []
@stack[module_name] << original_constants
module_name
@@ -262,7 +253,7 @@ module ActiveSupport #:nodoc:
end
def load_dependency(file)
- if Dependencies.load? && ActiveSupport::Dependencies.constant_watch_stack.watching?
+ if Dependencies.load? && Dependencies.constant_watch_stack.watching?
Dependencies.new_constants_in(Object) { yield }
else
yield
@@ -352,7 +343,6 @@ module ActiveSupport #:nodoc:
end
def clear
- log_call
Dependencies.unload_interlock do
loaded.clear
loading.clear
@@ -361,7 +351,6 @@ module ActiveSupport #:nodoc:
end
def require_or_load(file_name, const_path = nil)
- log_call file_name, const_path
file_name = $` if file_name =~ /\.rb\z/
expanded = File.expand_path(file_name)
return if loaded.include?(expanded)
@@ -377,8 +366,6 @@ module ActiveSupport #:nodoc:
begin
if load?
- log "loading #{file_name}"
-
# Enable warnings if this file has not been loaded before and
# warnings_on_first_load is set.
load_args = ["#{file_name}.rb"]
@@ -390,7 +377,6 @@ module ActiveSupport #:nodoc:
enable_warnings { result = load_file(*load_args) }
end
else
- log "requiring #{file_name}"
result = require file_name
end
rescue Exception
@@ -483,7 +469,6 @@ module ActiveSupport #:nodoc:
# set of names that the file at +path+ may define. See
# +loadable_constants_for_path+ for more details.
def load_file(path, const_paths = loadable_constants_for_path(path))
- log_call path, const_paths
const_paths = [const_paths].compact unless const_paths.is_a? Array
parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object }
@@ -494,7 +479,6 @@ module ActiveSupport #:nodoc:
autoloaded_constants.concat newly_defined_paths unless load_once_path?(path)
autoloaded_constants.uniq!
- log "loading #{path} defined #{newly_defined_paths * ', '}" unless newly_defined_paths.empty?
result
end
@@ -508,8 +492,6 @@ module ActiveSupport #:nodoc:
# it is not possible to load the constant into from_mod, try its parent
# module using +const_missing+.
def load_missing_constant(from_mod, const_name)
- log_call from_mod, const_name
-
unless qualified_const_defined?(from_mod.name) && Inflector.constantize(from_mod.name).equal?(from_mod)
raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
end
@@ -673,25 +655,20 @@ module ActiveSupport #:nodoc:
# exception, any new constants are regarded as being only partially defined
# and will be removed immediately.
def new_constants_in(*descs)
- log_call(*descs)
-
constant_watch_stack.watch_namespaces(descs)
- aborting = true
+ success = false
begin
yield # Now yield to the code that is to define new constants.
- aborting = false
+ success = true
ensure
new_constants = constant_watch_stack.new_constants
- log "New constants: #{new_constants * ', '}"
- return new_constants unless aborting
+ return new_constants if success
- log "Error during loading, removing partially loaded constants "
- new_constants.each { |c| remove_constant(c) }.clear
+ # Remove partially loaded constants.
+ new_constants.each { |c| remove_constant(c) }
end
-
- []
end
# Convert the provided const desc to a qualified constant name (as a string).
@@ -738,8 +715,6 @@ module ActiveSupport #:nodoc:
parent = constantize(parent_name)
end
- log "removing constant #{const}"
-
# In an autoloaded user.rb like this
#
# autoload :Foo, 'foo'
@@ -760,7 +735,7 @@ module ActiveSupport #:nodoc:
begin
constantized = parent.const_get(to_remove, false)
rescue NameError
- log "the constant #{const} is not reachable anymore, skipping"
+ # The constant is no longer reachable, just skip it.
return
else
constantized.before_remove_const if constantized.respond_to?(:before_remove_const)
@@ -770,27 +745,9 @@ module ActiveSupport #:nodoc:
begin
parent.instance_eval { remove_const to_remove }
rescue NameError
- log "the constant #{const} is not reachable anymore, skipping"
+ # The constant is no longer reachable, just skip it.
end
end
-
- protected
- def log_call(*args)
- if log_activity?
- arg_str = args.collect(&:inspect) * ', '
- /in `([a-z_\?\!]+)'/ =~ caller(1).first
- selector = $1 || '<unknown>'
- log "called #{selector}(#{arg_str})"
- end
- end
-
- def log(msg)
- logger.debug "Dependencies: #{msg}" if log_activity?
- end
-
- def log_activity?
- logger && log_activity
- end
end
end
diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb
index 47bcecbb35..f1865ca2f8 100644
--- a/activesupport/lib/active_support/dependencies/interlock.rb
+++ b/activesupport/lib/active_support/dependencies/interlock.rb
@@ -19,14 +19,12 @@ module ActiveSupport #:nodoc:
end
end
- # Attempt to obtain an "unloading" (exclusive) lock. If possible,
- # execute the supplied block while holding the lock. If there is
- # concurrent activity, return immediately (without executing the
- # block) instead of waiting.
- def attempt_unloading
- @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload], no_wait: true) do
- yield
- end
+ def start_unloading
+ @lock.start_exclusive(purpose: :unload, compatible: [:load, :unload])
+ end
+
+ def done_unloading
+ @lock.stop_exclusive(compatible: [:load, :unload])
end
def start_running
diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb
index f89fc0fe14..de5b233679 100644
--- a/activesupport/lib/active_support/deprecation/reporting.rb
+++ b/activesupport/lib/active_support/deprecation/reporting.rb
@@ -1,3 +1,5 @@
+require 'rbconfig'
+
module ActiveSupport
class Deprecation
module Reporting
@@ -63,7 +65,6 @@ module ActiveSupport
def deprecation_message(callstack, message = nil)
message ||= "You are using deprecated behavior which will be removed from the next major or minor release."
- message += '.' unless message =~ /\.$/
"DEPRECATION WARNING: #{message} #{deprecation_caller_message(callstack)}"
end
@@ -81,17 +82,17 @@ module ActiveSupport
def extract_callstack(callstack)
return _extract_callstack(callstack) if callstack.first.is_a? String
- rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/"
offending_line = callstack.find { |frame|
- frame.absolute_path && !frame.absolute_path.start_with?(rails_gem_root)
+ frame.absolute_path && !ignored_callstack(frame.absolute_path)
} || callstack.first
+
[offending_line.path, offending_line.lineno, offending_line.label]
end
def _extract_callstack(callstack)
warn "Please pass `caller_locations` to the deprecation API" if $VERBOSE
- rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/"
- offending_line = callstack.find { |line| !line.start_with?(rails_gem_root) } || callstack.first
+ offending_line = callstack.find { |line| !ignored_callstack(line) } || callstack.first
+
if offending_line
if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
md.captures
@@ -100,6 +101,12 @@ module ActiveSupport
end
end
end
+
+ RAILS_GEM_ROOT = File.expand_path("../../../../..", __FILE__) + "/"
+
+ def ignored_callstack(path)
+ path.start_with?(RAILS_GEM_ROOT) || path.start_with?(RbConfig::CONFIG['rubylibdir'])
+ end
end
end
end
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index c63b61e97a..3bde541009 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -9,6 +9,9 @@ module ActiveSupport
class Duration
attr_accessor :value, :parts
+ autoload :ISO8601Parser, 'active_support/duration/iso8601_parser'
+ autoload :ISO8601Serializer, 'active_support/duration/iso8601_serializer'
+
def initialize(value, parts) #:nodoc:
@value, @parts = value, parts
end
@@ -117,7 +120,7 @@ module ActiveSupport
def inspect #:nodoc:
parts.
reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
- sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
+ sort_by {|unit, _ | [:years, :months, :weeks, :days, :hours, :minutes, :seconds].index(unit)}.
map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
to_sentence(locale: ::I18n.default_locale)
end
@@ -130,6 +133,23 @@ module ActiveSupport
@value.respond_to?(method, include_private)
end
+ # Creates a new Duration from string formatted according to ISO 8601 Duration.
+ #
+ # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
+ # This method allows negative parts to be present in pattern.
+ # If invalid string is provided, it will raise +ActiveSupport::Duration::ISO8601Parser::ParsingError+.
+ def self.parse(iso8601duration)
+ parts = ISO8601Parser.new(iso8601duration).parse!
+ time = ::Time.current
+ new(time.advance(parts) - time, parts)
+ end
+
+ # Build ISO 8601 Duration string for this duration.
+ # The +precision+ parameter can be used to limit seconds' precision of duration.
+ def iso8601(precision: nil)
+ ISO8601Serializer.new(self, precision: precision).serialize
+ end
+
delegate :<=>, to: :value
protected
@@ -139,6 +159,8 @@ module ActiveSupport
if t.acts_like?(:time) || t.acts_like?(:date)
if type == :seconds
t.since(sign * number)
+ elsif [:hours, :minutes].include?(type)
+ t.in_time_zone.advance(type => sign * number)
else
t.advance(type => sign * number)
end
diff --git a/activesupport/lib/active_support/duration/iso8601_parser.rb b/activesupport/lib/active_support/duration/iso8601_parser.rb
new file mode 100644
index 0000000000..07af58ad99
--- /dev/null
+++ b/activesupport/lib/active_support/duration/iso8601_parser.rb
@@ -0,0 +1,122 @@
+require 'strscan'
+
+module ActiveSupport
+ class Duration
+ # Parses a string formatted according to ISO 8601 Duration into the hash.
+ #
+ # See {ISO 8601}[http://en.wikipedia.org/wiki/ISO_8601#Durations] for more information.
+ #
+ # This parser allows negative parts to be present in pattern.
+ class ISO8601Parser # :nodoc:
+ class ParsingError < ::ArgumentError; end
+
+ PERIOD_OR_COMMA = /\.|,/
+ PERIOD = '.'.freeze
+ COMMA = ','.freeze
+
+ SIGN_MARKER = /\A\-|\+|/
+ DATE_MARKER = /P/
+ TIME_MARKER = /T/
+ DATE_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(Y|M|D|W)/
+ TIME_COMPONENT = /(\-?\d+(?:[.,]\d+)?)(H|M|S)/
+
+ DATE_TO_PART = { 'Y' => :years, 'M' => :months, 'W' => :weeks, 'D' => :days }
+ TIME_TO_PART = { 'H' => :hours, 'M' => :minutes, 'S' => :seconds }
+
+ DATE_COMPONENTS = [:years, :months, :days]
+ TIME_COMPONENTS = [:hours, :minutes, :seconds]
+
+ attr_reader :parts, :scanner
+ attr_accessor :mode, :sign
+
+ def initialize(string)
+ @scanner = StringScanner.new(string)
+ @parts = {}
+ @mode = :start
+ @sign = 1
+ end
+
+ def parse!
+ while !finished?
+ case mode
+ when :start
+ if scan(SIGN_MARKER)
+ self.sign = (scanner.matched == '-') ? -1 : 1
+ self.mode = :sign
+ else
+ raise_parsing_error
+ end
+
+ when :sign
+ if scan(DATE_MARKER)
+ self.mode = :date
+ else
+ raise_parsing_error
+ end
+
+ when :date
+ if scan(TIME_MARKER)
+ self.mode = :time
+ elsif scan(DATE_COMPONENT)
+ parts[DATE_TO_PART[scanner[2]]] = number * sign
+ else
+ raise_parsing_error
+ end
+
+ when :time
+ if scan(TIME_COMPONENT)
+ parts[TIME_TO_PART[scanner[2]]] = number * sign
+ else
+ raise_parsing_error
+ end
+
+ end
+ end
+
+ validate!
+ parts
+ end
+
+ private
+
+ def finished?
+ scanner.eos?
+ end
+
+ # Parses number which can be a float with either comma or period.
+ def number
+ scanner[1] =~ PERIOD_OR_COMMA ? scanner[1].tr(COMMA, PERIOD).to_f : scanner[1].to_i
+ end
+
+ def scan(pattern)
+ scanner.scan(pattern)
+ end
+
+ def raise_parsing_error(reason = nil)
+ raise ParsingError, "Invalid ISO 8601 duration: #{scanner.string.inspect} #{reason}".strip
+ end
+
+ # Checks for various semantic errors as stated in ISO 8601 standard.
+ def validate!
+ raise_parsing_error('is empty duration') if parts.empty?
+
+ # Mixing any of Y, M, D with W is invalid.
+ if parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
+ raise_parsing_error('mixing weeks with other date parts not allowed')
+ end
+
+ # Specifying an empty T part is invalid.
+ if mode == :time && (parts.keys & TIME_COMPONENTS).empty?
+ raise_parsing_error('time part marker is present but time part is empty')
+ end
+
+ fractions = parts.values.reject(&:zero?).select { |a| (a % 1) != 0 }
+ unless fractions.empty? || (fractions.size == 1 && fractions.last == @parts.values.reject(&:zero?).last)
+ raise_parsing_error '(only last part can be fractional)'
+ end
+
+ return true
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/duration/iso8601_serializer.rb b/activesupport/lib/active_support/duration/iso8601_serializer.rb
new file mode 100644
index 0000000000..05c6a083a9
--- /dev/null
+++ b/activesupport/lib/active_support/duration/iso8601_serializer.rb
@@ -0,0 +1,51 @@
+require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/hash/transform_values'
+
+module ActiveSupport
+ class Duration
+ # Serializes duration to string according to ISO 8601 Duration format.
+ class ISO8601Serializer
+ def initialize(duration, precision: nil)
+ @duration = duration
+ @precision = precision
+ end
+
+ # Builds and returns output string.
+ def serialize
+ output = 'P'
+ parts, sign = normalize
+ output << "#{parts[:years]}Y" if parts.key?(:years)
+ output << "#{parts[:months]}M" if parts.key?(:months)
+ output << "#{parts[:weeks]}W" if parts.key?(:weeks)
+ output << "#{parts[:days]}D" if parts.key?(:days)
+ time = ''
+ time << "#{parts[:hours]}H" if parts.key?(:hours)
+ time << "#{parts[:minutes]}M" if parts.key?(:minutes)
+ if parts.key?(:seconds)
+ time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
+ end
+ output << "T#{time}" if time.present?
+ "#{sign}#{output}"
+ end
+
+ private
+
+ # Return pair of duration's parts and whole duration sign.
+ # Parts are summarized (as they can become repetitive due to addition, etc).
+ # Zero parts are removed as not significant.
+ # If all parts are negative it will negate all of them and return minus as a sign.
+ def normalize
+ parts = @duration.parts.each_with_object(Hash.new(0)) do |(k,v),p|
+ p[k] += v unless v.zero?
+ end
+ # If all parts are negative - let's make a negative duration
+ sign = ''
+ if parts.values.all? { |v| v < 0 }
+ sign = '-'
+ parts.transform_values!(&:-@)
+ end
+ [parts, sign]
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb
index 315be85fb3..6a02a838b7 100644
--- a/activesupport/lib/active_support/evented_file_update_checker.rb
+++ b/activesupport/lib/active_support/evented_file_update_checker.rb
@@ -21,7 +21,13 @@ module ActiveSupport
# Loading listen triggers warnings. These are originated by a legit
# usage of attr_* macros for private attributes, but adds a lot of noise
# to our test suite. Thus, we lazy load it and disable warnings locally.
- silence_warnings { require 'listen' }
+ silence_warnings do
+ begin
+ require 'listen'
+ rescue LoadError => e
+ raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
+ end
+ end
Listen.to(*dtw, &method(:changed)).start
end
end
@@ -37,6 +43,7 @@ module ActiveSupport
def execute_if_updated
if updated?
+ yield if block_given?
execute
true
end
diff --git a/activesupport/lib/active_support/execution_wrapper.rb b/activesupport/lib/active_support/execution_wrapper.rb
new file mode 100644
index 0000000000..00c5745a25
--- /dev/null
+++ b/activesupport/lib/active_support/execution_wrapper.rb
@@ -0,0 +1,117 @@
+require 'active_support/callbacks'
+
+module ActiveSupport
+ class ExecutionWrapper
+ include ActiveSupport::Callbacks
+
+ Null = Object.new # :nodoc:
+ def Null.complete! # :nodoc:
+ end
+
+ define_callbacks :run
+ define_callbacks :complete
+
+ def self.to_run(*args, &block)
+ set_callback(:run, *args, &block)
+ end
+
+ def self.to_complete(*args, &block)
+ set_callback(:complete, *args, &block)
+ end
+
+ # Register an object to be invoked during both the +run+ and
+ # +complete+ steps.
+ #
+ # +hook.complete+ will be passed the value returned from +hook.run+,
+ # and will only be invoked if +run+ has previously been called.
+ # (Mostly, this means it won't be invoked if an exception occurs in
+ # a preceding +to_run+ block; all ordinary +to_complete+ blocks are
+ # invoked in that situation.)
+ def self.register_hook(hook, outer: false)
+ if outer
+ run_args = [prepend: true]
+ complete_args = [:after]
+ else
+ run_args = complete_args = []
+ end
+
+ to_run(*run_args) do
+ hook_state[hook] = hook.run
+ end
+ to_complete(*complete_args) do
+ if hook_state.key?(hook)
+ hook.complete hook_state[hook]
+ end
+ end
+ end
+
+ # Run this execution.
+ #
+ # Returns an instance, whose +complete!+ method *must* be invoked
+ # after the work has been performed.
+ #
+ # Where possible, prefer +wrap+.
+ def self.run!
+ if active?
+ Null
+ else
+ new.tap do |instance|
+ success = nil
+ begin
+ instance.run!
+ success = true
+ ensure
+ instance.complete! unless success
+ end
+ end
+ end
+ end
+
+ # Perform the work in the supplied block as an execution.
+ def self.wrap
+ return yield if active?
+
+ instance = run!
+ begin
+ yield
+ ensure
+ instance.complete!
+ end
+ end
+
+ class << self # :nodoc:
+ attr_accessor :active
+ end
+
+ def self.inherited(other) # :nodoc:
+ super
+ other.active = Concurrent::Hash.new
+ end
+
+ self.active = Concurrent::Hash.new
+
+ def self.active? # :nodoc:
+ @active[Thread.current]
+ end
+
+ def run! # :nodoc:
+ self.class.active[Thread.current] = true
+ run_callbacks(:run)
+ end
+
+ # Complete this in-flight execution. This method *must* be called
+ # exactly once on the result of any call to +run!+.
+ #
+ # Where possible, prefer +wrap+.
+ def complete!
+ run_callbacks(:complete)
+ ensure
+ self.class.active.delete Thread.current
+ end
+
+ private
+ def hook_state
+ @_hook_state ||= {}
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/executor.rb b/activesupport/lib/active_support/executor.rb
new file mode 100644
index 0000000000..602fb11a44
--- /dev/null
+++ b/activesupport/lib/active_support/executor.rb
@@ -0,0 +1,6 @@
+require 'active_support/execution_wrapper'
+
+module ActiveSupport
+ class Executor < ExecutionWrapper
+ end
+end
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index 1fa9335080..b5667b6ac8 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/time/calculations'
+
module ActiveSupport
# FileUpdateChecker specifies the API used by Rails to watch files
# and control reloading. The API depends on four methods:
@@ -23,7 +25,7 @@ module ActiveSupport
# I18n.reload!
# end
#
- # ActionDispatch::Reloader.to_prepare do
+ # ActiveSupport::Reloader.to_prepare do
# i18n_reloader.execute_if_updated
# end
class FileUpdateChecker
@@ -81,6 +83,7 @@ module ActiveSupport
# Execute the block given if updated.
def execute_if_updated
if updated?
+ yield if block_given?
execute
true
else
@@ -111,7 +114,24 @@ module ActiveSupport
# reloading is not triggered.
def max_mtime(paths)
time_now = Time.now
- paths.map {|path| File.mtime(path)}.reject {|mtime| time_now < mtime}.max
+ max_mtime = nil
+
+ # Time comparisons are performed with #compare_without_coercion because
+ # AS redefines these operators in a way that is much slower and does not
+ # bring any benefit in this particular code.
+ #
+ # Read t1.compare_without_coercion(t2) < 0 as t1 < t2.
+ paths.each do |path|
+ mtime = File.mtime(path)
+
+ next if time_now.compare_without_coercion(mtime) < 0
+
+ if max_mtime.nil? || max_mtime.compare_without_coercion(mtime) < 0
+ max_mtime = mtime
+ end
+ end
+
+ max_mtime
end
def compile_glob(hash)
diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb
index fc08273b6d..4166ffc2fb 100644
--- a/activesupport/lib/active_support/gem_version.rb
+++ b/activesupport/lib/active_support/gem_version.rb
@@ -8,7 +8,7 @@ module ActiveSupport
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 82aacf3b24..6cc7c90c12 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -64,8 +64,8 @@ module I18n
end
app.reloaders << reloader
- ActionDispatch::Reloader.to_prepare do
- reloader.execute_if_updated
+ app.reloader.to_run do
+ reloader.execute_if_updated { require_unload_lock! }
# TODO: remove the following line as soon as the return value of
# callbacks is ignored, that is, returning `false` does not
# display a deprecation warning or halts the callback chain.
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index f741c0bfb8..f94e12e14f 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -140,6 +140,15 @@ module ActiveSupport
result
end
+ # Converts just the first character to uppercase.
+ #
+ # upcase_first('what a Lovely Day') # => "What a Lovely Day"
+ # upcase_first('w') # => "W"
+ # upcase_first('') # => ""
+ def upcase_first(string)
+ string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ''
+ end
+
# Capitalizes all the words and replaces some characters in the string to
# create a nicer looking title. +titleize+ is meant for creating pretty
# output. It is not used in the Rails internals.
diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb
index 7626b28108..de48e717b6 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -1,8 +1,10 @@
require 'active_support/logger_silence'
+require 'active_support/logger_thread_safe_level'
require 'logger'
module ActiveSupport
class Logger < ::Logger
+ include ActiveSupport::LoggerThreadSafeLevel
include LoggerSilence
# Returns true if the logger destination matches one of the sources
@@ -48,6 +50,11 @@ module ActiveSupport
logger.level = level
super(level)
end
+
+ define_method(:local_level=) do |level|
+ logger.local_level = level if logger.respond_to?(:local_level=)
+ super(level) if respond_to?(:local_level=)
+ end
end
end
diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb
index 125d81d973..3eb8098c77 100644
--- a/activesupport/lib/active_support/logger_silence.rb
+++ b/activesupport/lib/active_support/logger_silence.rb
@@ -7,39 +7,22 @@ module LoggerSilence
included do
cattr_accessor :silencer
- attr_reader :local_levels
self.silencer = true
end
- def after_initialize
- @local_levels = Concurrent::Map.new(:initial_capacity => 2)
- end
-
- def local_log_id
- Thread.current.__id__
- end
-
- def level
- local_levels[local_log_id] || super
- end
-
# Silences the logger for the duration of the block.
def silence(temporary_level = Logger::ERROR)
if silencer
begin
- old_local_level = local_levels[local_log_id]
- local_levels[local_log_id] = temporary_level
+ old_local_level = local_level
+ self.local_level = temporary_level
yield self
ensure
- if old_local_level
- local_levels[local_log_id] = old_local_level
- else
- local_levels.delete(local_log_id)
- end
+ self.local_level = old_local_level
end
else
yield self
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/logger_thread_safe_level.rb b/activesupport/lib/active_support/logger_thread_safe_level.rb
new file mode 100644
index 0000000000..5fedb5e689
--- /dev/null
+++ b/activesupport/lib/active_support/logger_thread_safe_level.rb
@@ -0,0 +1,31 @@
+require 'active_support/concern'
+
+module ActiveSupport
+ module LoggerThreadSafeLevel # :nodoc:
+ extend ActiveSupport::Concern
+
+ def after_initialize
+ @local_levels = Concurrent::Map.new(initial_capacity: 2)
+ end
+
+ def local_log_id
+ Thread.current.__id__
+ end
+
+ def local_level
+ @local_levels[local_log_id]
+ end
+
+ def local_level=(level)
+ if level
+ @local_levels[local_log_id] = level
+ else
+ @local_levels.delete(local_log_id)
+ end
+ end
+
+ def level
+ local_level || super
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb
index 854029bf83..4c3deffe6e 100644
--- a/activesupport/lib/active_support/message_verifier.rb
+++ b/activesupport/lib/active_support/message_verifier.rb
@@ -24,6 +24,12 @@ module ActiveSupport
# hash upon initialization:
#
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML)
+ #
+ # +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
+ # If you want to use a different hash algorithm, you can change it by providing
+ # `:digest` key as an option while initializing the verifier:
+ #
+ # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
class MessageVerifier
class InvalidSignature < StandardError; end
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 64d9e71f37..7a49bbb960 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -15,7 +15,7 @@ module ActiveSupport
extend self
- # Formats a +number+ into a US phone number (e.g., (555)
+ # Formats a +number+ into a phone number (US by default e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
#
# ==== Options
@@ -27,19 +27,26 @@ module ActiveSupport
# end of the generated number.
# * <tt>:country_code</tt> - Sets the country code for the phone
# number.
+ # * <tt>:pattern</tt> - Specifies how the number is divided into three
+ # groups with the custom regexp to override the default format.
# ==== Examples
#
- # number_to_phone(5551234) # => 555-1234
- # number_to_phone('5551234') # => 555-1234
- # number_to_phone(1235551234) # => 123-555-1234
- # number_to_phone(1235551234, area_code: true) # => (123) 555-1234
- # number_to_phone(1235551234, delimiter: ' ') # => 123 555 1234
- # number_to_phone(1235551234, area_code: true, extension: 555) # => (123) 555-1234 x 555
- # number_to_phone(1235551234, country_code: 1) # => +1-123-555-1234
- # number_to_phone('123a456') # => 123a456
+ # number_to_phone(5551234) # => "555-1234"
+ # number_to_phone('5551234') # => "555-1234"
+ # number_to_phone(1235551234) # => "123-555-1234"
+ # number_to_phone(1235551234, area_code: true) # => "(123) 555-1234"
+ # number_to_phone(1235551234, delimiter: ' ') # => "123 555 1234"
+ # number_to_phone(1235551234, area_code: true, extension: 555) # => "(123) 555-1234 x 555"
+ # number_to_phone(1235551234, country_code: 1) # => "+1-123-555-1234"
+ # number_to_phone('123a456') # => "123a456"
#
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
- # # => +1.123.555.1234 x 1343
+ # # => "+1.123.555.1234 x 1343"
+ #
+ # number_to_phone(75561234567, pattern: /(\d{1,4})(\d{4})(\d{4})$/, area_code: true)
+ # # => "(755) 6123-4567"
+ # number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})$/))
+ # # => "133-1234-5678"
def number_to_phone(number, options = {})
NumberToPhoneConverter.convert(number, options)
end
@@ -78,18 +85,18 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_currency(1234567890.50) # => $1,234,567,890.50
- # number_to_currency(1234567890.506) # => $1,234,567,890.51
- # number_to_currency(1234567890.506, precision: 3) # => $1,234,567,890.506
- # number_to_currency(1234567890.506, locale: :fr) # => 1 234 567 890,51 €
- # number_to_currency('123a456') # => $123a456
+ # number_to_currency(1234567890.50) # => "$1,234,567,890.50"
+ # number_to_currency(1234567890.506) # => "$1,234,567,890.51"
+ # number_to_currency(1234567890.506, precision: 3) # => "$1,234,567,890.506"
+ # number_to_currency(1234567890.506, locale: :fr) # => "1 234 567 890,51 €"
+ # number_to_currency('123a456') # => "$123a456"
#
# number_to_currency(-1234567890.50, negative_format: '(%u%n)')
- # # => ($1,234,567,890.50)
+ # # => "($1,234,567,890.50)"
# number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '')
- # # => &pound;1234567890,50
+ # # => "&pound;1234567890,50"
# number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
- # # => 1234567890,50 &pound;
+ # # => "1234567890,50 &pound;"
def number_to_currency(number, options = {})
NumberToCurrencyConverter.convert(number, options)
end
@@ -118,15 +125,15 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_percentage(100) # => 100.000%
- # number_to_percentage('98') # => 98.000%
- # number_to_percentage(100, precision: 0) # => 100%
- # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
- # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
- # number_to_percentage(1000, locale: :fr) # => 1000,000%
- # number_to_percentage(1000, precision: nil) # => 1000%
- # number_to_percentage('98a') # => 98a%
- # number_to_percentage(100, format: '%n %') # => 100.000 %
+ # number_to_percentage(100) # => "100.000%"
+ # number_to_percentage('98') # => "98.000%"
+ # number_to_percentage(100, precision: 0) # => "100%"
+ # number_to_percentage(1000, delimiter: '.', separator: ',') # => "1.000,000%"
+ # number_to_percentage(302.24398923423, precision: 5) # => "302.24399%"
+ # number_to_percentage(1000, locale: :fr) # => "1000,000%"
+ # number_to_percentage(1000, precision: nil) # => "1000%"
+ # number_to_percentage('98a') # => "98a%"
+ # number_to_percentage(100, format: '%n %') # => "100.000 %"
def number_to_percentage(number, options = {})
NumberToPercentageConverter.convert(number, options)
end
@@ -149,19 +156,19 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_delimited(12345678) # => 12,345,678
- # number_to_delimited('123456') # => 123,456
- # number_to_delimited(12345678.05) # => 12,345,678.05
- # number_to_delimited(12345678, delimiter: '.') # => 12.345.678
- # number_to_delimited(12345678, delimiter: ',') # => 12,345,678
- # number_to_delimited(12345678.05, separator: ' ') # => 12,345,678 05
- # number_to_delimited(12345678.05, locale: :fr) # => 12 345 678,05
- # number_to_delimited('112a') # => 112a
+ # number_to_delimited(12345678) # => "12,345,678"
+ # number_to_delimited('123456') # => "123,456"
+ # number_to_delimited(12345678.05) # => "12,345,678.05"
+ # number_to_delimited(12345678, delimiter: '.') # => "12.345.678"
+ # number_to_delimited(12345678, delimiter: ',') # => "12,345,678"
+ # number_to_delimited(12345678.05, separator: ' ') # => "12,345,678 05"
+ # number_to_delimited(12345678.05, locale: :fr) # => "12 345 678,05"
+ # number_to_delimited('112a') # => "112a"
# number_to_delimited(98765432.98, delimiter: ' ', separator: ',')
- # # => 98 765 432,98
+ # # => "98 765 432,98"
# number_to_delimited("123456.78",
# delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/)
- # # => 1,23,456.78
+ # # => "1,23,456.78"
def number_to_delimited(number, options = {})
NumberToDelimitedConverter.convert(number, options)
end
@@ -190,22 +197,22 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_rounded(111.2345) # => 111.235
- # number_to_rounded(111.2345, precision: 2) # => 111.23
- # number_to_rounded(13, precision: 5) # => 13.00000
- # number_to_rounded(389.32314, precision: 0) # => 389
- # number_to_rounded(111.2345, significant: true) # => 111
- # number_to_rounded(111.2345, precision: 1, significant: true) # => 100
- # number_to_rounded(13, precision: 5, significant: true) # => 13.000
- # number_to_rounded(13, precision: nil) # => 13
- # number_to_rounded(111.234, locale: :fr) # => 111,234
+ # number_to_rounded(111.2345) # => "111.235"
+ # number_to_rounded(111.2345, precision: 2) # => "111.23"
+ # number_to_rounded(13, precision: 5) # => "13.00000"
+ # number_to_rounded(389.32314, precision: 0) # => "389"
+ # number_to_rounded(111.2345, significant: true) # => "111"
+ # number_to_rounded(111.2345, precision: 1, significant: true) # => "100"
+ # number_to_rounded(13, precision: 5, significant: true) # => "13.000"
+ # number_to_rounded(13, precision: nil) # => "13"
+ # number_to_rounded(111.234, locale: :fr) # => "111,234"
#
# number_to_rounded(13, precision: 5, significant: true, strip_insignificant_zeros: true)
- # # => 13
+ # # => "13"
#
- # number_to_rounded(389.32314, precision: 4, significant: true) # => 389.3
+ # number_to_rounded(389.32314, precision: 4, significant: true) # => "389.3"
# number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.')
- # # => 1.111,23
+ # # => "1.111,23"
def number_to_rounded(number, options = {})
NumberToRoundedConverter.convert(number, options)
end
@@ -237,17 +244,17 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_human_size(123) # => 123 Bytes
- # number_to_human_size(1234) # => 1.21 KB
- # number_to_human_size(12345) # => 12.1 KB
- # number_to_human_size(1234567) # => 1.18 MB
- # number_to_human_size(1234567890) # => 1.15 GB
- # number_to_human_size(1234567890123) # => 1.12 TB
- # number_to_human_size(1234567890123456) # => 1.1 PB
- # number_to_human_size(1234567890123456789) # => 1.07 EB
- # number_to_human_size(1234567, precision: 2) # => 1.2 MB
- # number_to_human_size(483989, precision: 2) # => 470 KB
- # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
+ # number_to_human_size(123) # => "123 Bytes"
+ # number_to_human_size(1234) # => "1.21 KB"
+ # number_to_human_size(12345) # => "12.1 KB"
+ # number_to_human_size(1234567) # => "1.18 MB"
+ # number_to_human_size(1234567890) # => "1.15 GB"
+ # number_to_human_size(1234567890123) # => "1.12 TB"
+ # number_to_human_size(1234567890123456) # => "1.1 PB"
+ # number_to_human_size(1234567890123456789) # => "1.07 EB"
+ # number_to_human_size(1234567, precision: 2) # => "1.2 MB"
+ # number_to_human_size(483989, precision: 2) # => "470 KB"
+ # number_to_human_size(1234567, precision: 2, separator: ',') # => "1,2 MB"
# number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
# number_to_human_size(524288000, precision: 5) # => "500 MB"
def number_to_human_size(number, options = {})
diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
index 45ae8f1a93..43c5540b6f 100644
--- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
@@ -12,7 +12,7 @@ module ActiveSupport
private
def parts
- left, right = number.to_s.split('.')
+ left, right = number.to_s.split('.'.freeze)
left.gsub!(delimiter_pattern) do |digit_to_delimit|
"#{digit_to_delimit}#{options[:delimiter]}"
end
diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
index af2ee56d91..dee74fa7a6 100644
--- a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
@@ -18,12 +18,16 @@ module ActiveSupport
end
def convert_with_area_code(number)
- number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
+ default_pattern = /(\d{1,3})(\d{3})(\d{4}$)/
+ number.gsub!(regexp_pattern(default_pattern),
+ "(\\1) \\2#{delimiter}\\3")
number
end
def convert_without_area_code(number)
- number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ default_pattern = /(\d{0,3})(\d{3})(\d{4})$/
+ number.gsub!(regexp_pattern(default_pattern),
+ "\\1#{delimiter}\\2#{delimiter}\\3")
number.slice!(0, 1) if start_with_delimiter?(number)
number
end
@@ -43,6 +47,11 @@ module ActiveSupport
def phone_ext(ext)
ext.blank? ? "" : " x #{ext}"
end
+
+ def regexp_pattern(default_pattern)
+ opts.fetch :pattern, default_pattern
+ end
+
end
end
end
diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
index 981c562551..9fb7dfb779 100644
--- a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
+++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
@@ -29,9 +29,11 @@ module ActiveSupport
formatted_string =
if BigDecimal === rounded_number && rounded_number.finite?
- s = rounded_number.to_s('F') + '0'*precision
- a, b = s.split('.', 2)
- a + '.' + b[0, precision]
+ s = rounded_number.to_s('F')
+ s << '0'.freeze * precision
+ a, b = s.split('.'.freeze, 2)
+ a << '.'.freeze
+ a << b[0, precision]
else
"%00.#{precision}f" % rounded_number
end
diff --git a/activesupport/lib/active_support/reloader.rb b/activesupport/lib/active_support/reloader.rb
new file mode 100644
index 0000000000..5623bdd349
--- /dev/null
+++ b/activesupport/lib/active_support/reloader.rb
@@ -0,0 +1,129 @@
+require 'active_support/execution_wrapper'
+
+module ActiveSupport
+ #--
+ # This class defines several callbacks:
+ #
+ # to_prepare -- Run once at application startup, and also from
+ # +to_run+.
+ #
+ # to_run -- Run before a work run that is reloading. If
+ # +reload_classes_only_on_change+ is true (the default), the class
+ # unload will have already occurred.
+ #
+ # to_complete -- Run after a work run that has reloaded. If
+ # +reload_classes_only_on_change+ is false, the class unload will
+ # have occurred after the work run, but before this callback.
+ #
+ # before_class_unload -- Run immediately before the classes are
+ # unloaded.
+ #
+ # after_class_unload -- Run immediately after the classes are
+ # unloaded.
+ #
+ class Reloader < ExecutionWrapper
+ define_callbacks :prepare
+
+ define_callbacks :class_unload
+
+ def self.to_prepare(*args, &block)
+ set_callback(:prepare, *args, &block)
+ end
+
+ def self.before_class_unload(*args, &block)
+ set_callback(:class_unload, *args, &block)
+ end
+
+ def self.after_class_unload(*args, &block)
+ set_callback(:class_unload, :after, *args, &block)
+ end
+
+ to_run(:after) { self.class.prepare! }
+
+ # Initiate a manual reload
+ def self.reload!
+ executor.wrap do
+ new.tap do |instance|
+ begin
+ instance.run!
+ ensure
+ instance.complete!
+ end
+ end
+ end
+ prepare!
+ end
+
+ def self.run! # :nodoc:
+ if check!
+ super
+ else
+ Null
+ end
+ end
+
+ # Run the supplied block as a work unit, reloading code as needed
+ def self.wrap
+ executor.wrap do
+ super
+ end
+ end
+
+ class_attribute :executor
+ class_attribute :check
+
+ self.executor = Executor
+ self.check = lambda { false }
+
+ def self.check! # :nodoc:
+ @should_reload ||= check.call
+ end
+
+ def self.reloaded! # :nodoc:
+ @should_reload = false
+ end
+
+ def self.prepare! # :nodoc:
+ new.run_callbacks(:prepare)
+ end
+
+ def initialize
+ super
+ @locked = false
+ end
+
+ # Acquire the ActiveSupport::Dependencies::Interlock unload lock,
+ # ensuring it will be released automatically
+ def require_unload_lock!
+ unless @locked
+ ActiveSupport::Dependencies.interlock.start_unloading
+ @locked = true
+ end
+ end
+
+ # Release the unload lock if it has been previously obtained
+ def release_unload_lock!
+ if @locked
+ @locked = false
+ ActiveSupport::Dependencies.interlock.done_unloading
+ end
+ end
+
+ def run! # :nodoc:
+ super
+ release_unload_lock!
+ end
+
+ def class_unload!(&block) # :nodoc:
+ require_unload_lock!
+ run_callbacks(:class_unload, &block)
+ end
+
+ def complete! # :nodoc:
+ super
+ self.class.reloaded!
+ ensure
+ release_unload_lock!
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index fcf5553061..73bc52b56f 100644
--- a/activesupport/lib/active_support/rescuable.rb
+++ b/activesupport/lib/active_support/rescuable.rb
@@ -115,5 +115,15 @@ module ActiveSupport
end
end
end
+
+ def index_of_handler_for_rescue(exception)
+ handlers = self.class.rescue_handlers.reverse_each.with_index
+ _, index = handlers.detect do |(klass_name, _), _|
+ klass = self.class.const_get(klass_name) rescue nil
+ klass ||= (klass_name.constantize rescue nil)
+ klass === exception if klass
+ end
+ index
+ end
end
end
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index d9a668c0ea..1fc12d0bc1 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -66,12 +66,20 @@ module ActiveSupport
alias :assert_not_respond_to :refute_respond_to
alias :assert_not_same :refute_same
- # Reveals the intention that the block should not raise any exception.
+
+ # Assertion that the block should not raise an exception.
+ #
+ # Passes if evaluated code in the yielded block raises no exception.
#
# assert_nothing_raised do
- # ...
+ # perform_service(param: 'no_exception')
# end
def assert_nothing_raised(*args)
+ if args.present?
+ ActiveSupport::Deprecation.warn(
+ "Passing arguments to assert_nothing_raised " \
+ "is deprecated and will be removed in Rails 5.1.")
+ end
yield
end
end
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index 7ca3592520..118bf8eab0 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -1,7 +1,6 @@
require 'tzinfo'
require 'concurrent/map'
require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/object/try'
module ActiveSupport
# The TimeZone class serves as a wrapper around TZInfo::Timezone instances.
diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb
index c0e23e89f7..7f0fcd6996 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -1,12 +1,5 @@
ORIG_ARGV = ARGV.dup
-begin
- old, $VERBOSE = $VERBOSE, nil
- require File.expand_path('../../../load_paths', __FILE__)
-ensure
- $VERBOSE = old
-end
-
require 'active_support/core_ext/kernel/reporting'
silence_warnings do
diff --git a/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
new file mode 100644
index 0000000000..3ca4213c71
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/raises_arbitrary_exception.rb
@@ -0,0 +1,4 @@
+RaisesArbitraryException = 1
+_ = A::B # Autoloading recursion, also expected to be watched and discarded.
+
+raise Exception, 'arbitray exception message'
diff --git a/activesupport/test/autoloading_fixtures/throws.rb b/activesupport/test/autoloading_fixtures/throws.rb
new file mode 100644
index 0000000000..e1d96cc512
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/throws.rb
@@ -0,0 +1,4 @@
+Throws = 1
+_ = A::B # Autoloading recursion, expected to be discarded.
+
+throw :t
diff --git a/activesupport/test/benchmarkable_test.rb b/activesupport/test/benchmarkable_test.rb
index 04d4f5e503..5af041f458 100644
--- a/activesupport/test/benchmarkable_test.rb
+++ b/activesupport/test/benchmarkable_test.rb
@@ -41,6 +41,20 @@ class BenchmarkableTest < ActiveSupport::TestCase
assert_last_logged 'test_run'
end
+ def test_with_silence
+ assert_difference 'buffer.count', +2 do
+ benchmark('test_run') do
+ logger.info "SOMETHING"
+ end
+ end
+
+ assert_difference 'buffer.count', +1 do
+ benchmark('test_run', silence: true) do
+ logger.info "NOTHING"
+ end
+ end
+ end
+
def test_within_level
logger.level = ActiveSupport::Logger::DEBUG
benchmark('included_debug_run', :level => :debug) { }
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index 4a299429f3..ec7d028d7e 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -266,6 +266,20 @@ module CacheStoreBehavior
end
end
+ def test_fetch_with_forced_cache_miss_with_block
+ @cache.write('foo', 'bar')
+ assert_equal 'foo_bar', @cache.fetch('foo', force: true) { 'foo_bar' }
+ end
+
+ def test_fetch_with_forced_cache_miss_without_block
+ @cache.write('foo', 'bar')
+ assert_raises(ArgumentError) do
+ @cache.fetch('foo', force: true)
+ end
+
+ assert_equal 'bar', @cache.read('foo')
+ end
+
def test_should_read_and_write_hash
assert @cache.write('foo', {:a => "b"})
assert_equal({:a => "b"}, @cache.read('foo'))
@@ -836,7 +850,7 @@ class FileStoreTest < ActiveSupport::TestCase
# If nothing has been stored in the cache, there is a chance the cache directory does not yet exist
# Ensure delete_matched gracefully handles this case
def test_delete_matched_when_cache_directory_does_not_exist
- assert_nothing_raised(Exception) do
+ assert_nothing_raised do
ActiveSupport::Cache::FileStore.new('/test/cache/directory').delete_matched(/does_not_exist/)
end
end
@@ -844,7 +858,7 @@ class FileStoreTest < ActiveSupport::TestCase
def test_delete_does_not_delete_empty_parent_dir
sub_cache_dir = File.join(cache_dir, 'subdir/')
sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir)
- assert_nothing_raised(Exception) do
+ assert_nothing_raised do
assert sub_cache_store.write('foo', 'bar')
assert sub_cache_store.delete('foo')
end
@@ -1151,15 +1165,6 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase
@cache.mute { @cache.fetch('foo') { 'bar' } }
assert @buffer.string.blank?
end
-
- def test_multi_read_loggin
- @cache.write 'hello', 'goodbye'
- @cache.write 'world', 'earth'
-
- @cache.read_multi('hello', 'world')
-
- assert_match "Caches multi read:\n- hello\n- world", @buffer.string
- end
end
class CacheEntryTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb
index 784547bdf8..54df87def8 100644
--- a/activesupport/test/core_ext/date_and_time_behavior.rb
+++ b/activesupport/test/core_ext/date_and_time_behavior.rb
@@ -301,6 +301,16 @@ module DateAndTimeBehavior
assert_not date_time_init(2015,1,5,15,15,10).on_weekend?
end
+ def test_on_weekday_on_sunday
+ assert_not date_time_init(2015,1,4,0,0,0).on_weekday?
+ assert_not date_time_init(2015,1,4,15,15,10).on_weekday?
+ end
+
+ def test_on_weekday_on_monday
+ assert date_time_init(2015,1,5,0,0,0).on_weekday?
+ assert date_time_init(2015,1,5,15,15,10).on_weekday?
+ end
+
def with_bw_default(bw = :monday)
old_bw = Date.beginning_of_week
Date.beginning_of_week = bw
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index 6fe38c45ec..16efeeadd5 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -186,6 +186,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2006,11,15), DateTime.civil(2006,11,23,0,0,0).last_week(:wednesday)
end
+ def test_date_time_should_have_correct_last_week_for_leap_year
+ assert_equal DateTime.civil(2016, 2, 29), DateTime.civil(2016, 3, 7).last_week
+ end
+
def test_last_month_on_31st
assert_equal DateTime.civil(2004, 2, 29), DateTime.civil(2004, 3, 31).last_month
end
@@ -350,6 +354,24 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal nil, DateTime.civil(2000) <=> "Invalid as Time"
end
+ def test_compare_with_integer
+ assert_equal 1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440587
+ assert_equal 0, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440588
+ assert_equal(-1, DateTime.civil(1970, 1, 1, 12, 0, 0) <=> 2440589)
+ end
+
+ def test_compare_with_float
+ assert_equal 1, DateTime.civil(1970) <=> 2440586.5
+ assert_equal 0, DateTime.civil(1970) <=> 2440587.5
+ assert_equal(-1, DateTime.civil(1970) <=> 2440588.5)
+ end
+
+ def test_compare_with_rational
+ assert_equal 1, DateTime.civil(1970) <=> Rational(4881173, 2)
+ assert_equal 0, DateTime.civil(1970) <=> Rational(4881175, 2)
+ assert_equal(-1, DateTime.civil(1970) <=> Rational(4881177, 2))
+ end
+
def test_to_f
assert_equal 946684800.0, DateTime.civil(2000).to_f
assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index 9e97acaffb..bef660fe12 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -66,8 +66,9 @@ class DurationTest < ActiveSupport::TestCase
assert_equal '10 years, 2 months, and 1 day', (10.years + 2.months + 1.day).inspect
assert_equal '10 years, 2 months, and 1 day', (10.years + 1.month + 1.day + 1.month).inspect
assert_equal '10 years, 2 months, and 1 day', (1.day + 10.years + 2.months).inspect
- assert_equal '7 days', 1.week.inspect
- assert_equal '14 days', 1.fortnight.inspect
+ assert_equal '7 days', 7.days.inspect
+ assert_equal '1 week', 1.week.inspect
+ assert_equal '2 weeks', 1.fortnight.inspect
end
def test_inspect_locale
@@ -222,4 +223,89 @@ class DurationTest < ActiveSupport::TestCase
assert_equal(1, (1.minute <=> 1.second))
assert_equal(1, (61 <=> 1.minute))
end
+
+ # ISO8601 string examples are taken from ISO8601 gem at https://github.com/arnau/ISO8601/blob/b93d466840/spec/iso8601/duration_spec.rb
+ # published under the conditions of MIT license at https://github.com/arnau/ISO8601/blob/b93d466840/LICENSE
+ #
+ # Copyright (c) 2012-2014 Arnau Siches
+ #
+ # MIT License
+ #
+ # Permission is hereby granted, free of charge, to any person obtaining
+ # a copy of this software and associated documentation files (the
+ # "Software"), to deal in the Software without restriction, including
+ # without limitation the rights to use, copy, modify, merge, publish,
+ # distribute, sublicense, and/or sell copies of the Software, and to
+ # permit persons to whom the Software is furnished to do so, subject to
+ # the following conditions:
+ #
+ # The above copyright notice and this permission notice shall be
+ # included in all copies or substantial portions of the Software.
+ #
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ def test_iso8601_parsing_wrong_patterns_with_raise
+ invalid_patterns = ['', 'P', 'PT', 'P1YT', 'T', 'PW', 'P1Y1W', '~P1Y', '.P1Y', 'P1.5Y0.5M', 'P1.5Y1M', 'P1.5MT10.5S']
+ invalid_patterns.each do |pattern|
+ assert_raise ActiveSupport::Duration::ISO8601Parser::ParsingError, pattern.inspect do
+ ActiveSupport::Duration.parse(pattern)
+ end
+ end
+ end
+
+ def test_iso8601_output
+ expectations = [
+ ['P1Y', 1.year ],
+ ['P1W', 1.week ],
+ ['P1Y1M', 1.year + 1.month ],
+ ['P1Y1M1D', 1.year + 1.month + 1.day ],
+ ['-P1Y1D', -1.year - 1.day ],
+ ['P1Y-1DT-1S', 1.year - 1.day - 1.second ], # Parts with different signs are exists in PostgreSQL interval datatype.
+ ['PT1S', 1.second ],
+ ['PT1.4S', (1.4).seconds ],
+ ['P1Y1M1DT1H', 1.year + 1.month + 1.day + 1.hour],
+ ]
+ expectations.each do |expected_output, duration|
+ assert_equal expected_output, duration.iso8601, expected_output.inspect
+ end
+ end
+
+ def test_iso8601_output_precision
+ expectations = [
+ [nil, 'P1Y1MT5.55S', 1.year + 1.month + (5.55).seconds ],
+ [0, 'P1Y1MT6S', 1.year + 1.month + (5.55).seconds ],
+ [1, 'P1Y1MT5.5S', 1.year + 1.month + (5.55).seconds ],
+ [2, 'P1Y1MT5.55S', 1.year + 1.month + (5.55).seconds ],
+ [3, 'P1Y1MT5.550S', 1.year + 1.month + (5.55).seconds ],
+ [nil, 'PT1S', 1.second ],
+ [2, 'PT1.00S', 1.second ],
+ [nil, 'PT1.4S', (1.4).seconds ],
+ [0, 'PT1S', (1.4).seconds ],
+ [1, 'PT1.4S', (1.4).seconds ],
+ [5, 'PT1.40000S', (1.4).seconds ],
+ ]
+ expectations.each do |precision, expected_output, duration|
+ assert_equal expected_output, duration.iso8601(precision: precision), expected_output.inspect
+ end
+ end
+
+ def test_iso8601_output_and_reparsing
+ patterns = %w[
+ P1Y P0.5Y P0,5Y P1Y1M P1Y0.5M P1Y0,5M P1Y1M1D P1Y1M0.5D P1Y1M0,5D P1Y1M1DT1H P1Y1M1DT0.5H P1Y1M1DT0,5H P1W +P1Y -P1Y
+ P1Y1M1DT1H1M P1Y1M1DT1H0.5M P1Y1M1DT1H0,5M P1Y1M1DT1H1M1S P1Y1M1DT1H1M1.0S P1Y1M1DT1H1M1,0S P-1Y-2M3DT-4H-5M-6S
+ ]
+ # That could be weird, but if we parse P1Y1M0.5D and output it to ISO 8601, we'll get P1Y1MT12.0H.
+ # So we check that initially parsed and reparsed duration added to time will result in the same time.
+ time = Time.current
+ patterns.each do |pattern|
+ duration = ActiveSupport::Duration.parse(pattern)
+ assert_equal time+duration, time+ActiveSupport::Duration.parse(duration.iso8601), pattern.inspect
+ end
+ end
end
diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb
index 99af274614..962d3a30b6 100644
--- a/activesupport/test/core_ext/hash/transform_keys_test.rb
+++ b/activesupport/test/core_ext/hash/transform_keys_test.rb
@@ -43,4 +43,20 @@ class TransformKeysTest < ActiveSupport::TestCase
original.transform_keys!.with_index { |k, i| [k, i].join.to_sym }
assert_equal({ a0: 'a', b1: 'b' }, original)
end
+
+ test "transform_keys returns a Hash instance when self is inherited from Hash" do
+ class HashDescendant < ::Hash
+ def initialize(elements = nil)
+ super(elements)
+ (elements || {}).each_pair{ |key, value| self[key] = value }
+ end
+ end
+
+ original = HashDescendant.new({ a: 'a', b: 'b' })
+ mapped = original.transform_keys { |k| "#{k}!".to_sym }
+
+ assert_equal({ a: 'a', b: 'b' }, original)
+ assert_equal({ a!: 'a', b!: 'b' }, mapped)
+ assert_equal(::Hash, mapped.class)
+ end
end
diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb
index 825df439a5..380f64c6fd 100644
--- a/activesupport/test/core_ext/marshal_test.rb
+++ b/activesupport/test/core_ext/marshal_test.rb
@@ -29,7 +29,12 @@ class MarshalTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
with_autoloading_fixtures do
- assert_kind_of EM, Marshal.load(dumped)
+ object = nil
+ assert_nothing_raised do
+ object = Marshal.load(dumped)
+ end
+
+ assert_kind_of EM, object
end
end
@@ -43,7 +48,12 @@ class MarshalTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
with_autoloading_fixtures do
- assert_kind_of ClassFolder::ClassFolderSubclass, Marshal.load(dumped)
+ object = nil
+ assert_nothing_raised do
+ object = Marshal.load(dumped)
+ end
+
+ assert_kind_of ClassFolder::ClassFolderSubclass, object
end
end
@@ -64,6 +74,17 @@ class MarshalTest < ActiveSupport::TestCase
end
end
+ test "when one constant resolves to another" do
+ class Parent; C = Class.new; end
+ class Child < Parent; C = Class.new; end
+
+ dump = Marshal.dump(Child::C.new)
+
+ Child.send(:remove_const, :C)
+
+ assert_raise(ArgumentError) { Marshal.load(dump) }
+ end
+
test "that a real missing class is causing an exception" do
dumped = nil
with_autoloading_fixtures do
@@ -96,7 +117,7 @@ class MarshalTest < ActiveSupport::TestCase
Marshal.load(dumped)
end
- assert_nothing_raised("EM failed to load while we expect only SomeClass to fail loading") do
+ assert_nothing_raised do
EM.new
end
@@ -117,7 +138,12 @@ class MarshalTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
with_autoloading_fixtures do
- assert_kind_of EM, Marshal.load(f)
+ object = nil
+ assert_nothing_raised do
+ object = Marshal.load(f)
+ end
+
+ assert_kind_of EM, object
end
end
end
diff --git a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
index 65fadc5c20..a9fd878b80 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
@@ -106,4 +106,10 @@ class ModuleAttributeAccessorPerThreadTest < ActiveSupport::TestCase
end
assert_equal "invalid attribute name: 2valid_part", exception.message
end
+
+ def test_should_return_same_value_by_class_or_instance_accessor
+ @class.foo = 'fries'
+
+ assert_equal @class.foo, @object.foo
+ end
end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index 0ed66f8c37..ae4aed0554 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -328,7 +328,13 @@ class ModuleTest < ActiveSupport::TestCase
end
def test_local_constants
- assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s)
+ ActiveSupport::Deprecation.silence do
+ assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s)
+ end
+ end
+
+ def test_local_constants_is_deprecated
+ assert_deprecated { Ab.local_constants.sort.map(&:to_s) }
end
end
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 2e69816364..f38b225b38 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -76,6 +76,18 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
end
+ def test_upcase_first
+ assert_equal "What a Lovely Day", "what a Lovely Day".upcase_first
+ end
+
+ def test_upcase_first_with_one_char
+ assert_equal "W", "w".upcase_first
+ end
+
+ def test_upcase_first_with_empty_string
+ assert_equal "", "".upcase_first
+ end
+
def test_camelize
CamelToUnderscore.each do |camel, underscore|
assert_equal(camel, underscore.camelize)
@@ -444,6 +456,7 @@ class StringConversionsTest < ActiveSupport::TestCase
assert_equal Time.local(2011, 2, 27, 17, 50), "2011-02-27 13:50 -0100".to_time
assert_equal Time.utc(2011, 2, 27, 23, 50), "2011-02-27 22:50 -0100".to_time(:utc)
assert_equal Time.local(2005, 2, 27, 22, 50), "2005-02-27 14:50 -0500".to_time
+ assert_nil "010".to_time
assert_nil "".to_time
end
end
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index e45df63fd5..d8bb38621b 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -392,7 +392,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,44).change(:usec => 8)
assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,2).change(:nsec => 8000)
assert_raise(ArgumentError) { Time.local(2005,1,2,11,22,33, 8).change(:usec => 1, :nsec => 1) }
- assert_nothing_raised(ArgumentError) { Time.new(2015, 5, 9, 10, 00, 00, '+03:00').change(nsec: 999999999) }
+ assert_nothing_raised { Time.new(2015, 5, 9, 10, 00, 00, '+03:00').change(nsec: 999999999) }
end
def test_utc_change
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index 757e600646..04e7b24d30 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -76,6 +76,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_dependency_which_raises_exception_isnt_added_to_loaded_set
with_loading do
filename = 'dependencies/raises_exception'
+ expanded = File.expand_path(filename)
$raises_exception_load_count = 0
5.times do |count|
@@ -86,8 +87,8 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal 'Loading me failed, so do not add to loaded or history.', e.message
assert_equal count + 1, $raises_exception_load_count
- assert_not ActiveSupport::Dependencies.loaded.include?(filename)
- assert_not ActiveSupport::Dependencies.history.include?(filename)
+ assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_not ActiveSupport::Dependencies.history.include?(expanded)
end
end
end
@@ -268,6 +269,28 @@ class DependenciesTest < ActiveSupport::TestCase
remove_constants(:ModuleFolder)
end
+ def test_raising_discards_autoloaded_constants
+ with_autoloading_fixtures do
+ assert_raises(Exception, 'arbitray exception message') { RaisesArbitraryException }
+ assert_not defined?(A)
+ assert_not defined?(RaisesArbitraryException)
+ end
+ ensure
+ remove_constants(:A, :RaisesArbitraryException)
+ end
+
+ def test_throwing_discards_autoloaded_constants
+ with_autoloading_fixtures do
+ catch :t do
+ Throws
+ end
+ assert_not defined?(A)
+ assert_not defined?(Throws)
+ end
+ ensure
+ remove_constants(:A, :Throws)
+ end
+
def test_doesnt_break_normal_require
path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
original_path = $:.dup
@@ -1047,12 +1070,4 @@ class DependenciesTest < ActiveSupport::TestCase
ensure
ActiveSupport::Dependencies.hook!
end
-
- def test_unhook
- ActiveSupport::Dependencies.unhook!
- assert !Module.new.respond_to?(:const_missing_without_dependencies)
- assert !Module.new.respond_to?(:load_without_new_constant_marking)
- ensure
- ActiveSupport::Dependencies.hook!
- end
end
diff --git a/activesupport/test/executor_test.rb b/activesupport/test/executor_test.rb
new file mode 100644
index 0000000000..d9b389461a
--- /dev/null
+++ b/activesupport/test/executor_test.rb
@@ -0,0 +1,183 @@
+require 'abstract_unit'
+
+class ExecutorTest < ActiveSupport::TestCase
+ class DummyError < RuntimeError
+ end
+
+ def test_wrap_invokes_callbacks
+ called = []
+ executor.to_run { called << :run }
+ executor.to_complete { called << :complete }
+
+ executor.wrap do
+ called << :body
+ end
+
+ assert_equal [:run, :body, :complete], called
+ end
+
+ def test_callbacks_share_state
+ result = false
+ executor.to_run { @foo = true }
+ executor.to_complete { result = @foo }
+
+ executor.wrap { }
+
+ assert result
+ end
+
+ def test_separated_calls_invoke_callbacks
+ called = []
+ executor.to_run { called << :run }
+ executor.to_complete { called << :complete }
+
+ state = executor.run!
+ called << :body
+ state.complete!
+
+ assert_equal [:run, :body, :complete], called
+ end
+
+ def test_exceptions_unwind
+ called = []
+ executor.to_run { called << :run_1 }
+ executor.to_run { raise DummyError }
+ executor.to_run { called << :run_2 }
+ executor.to_complete { called << :complete }
+
+ assert_raises(DummyError) do
+ executor.wrap { called << :body }
+ end
+
+ assert_equal [:run_1, :complete], called
+ end
+
+ def test_avoids_double_wrapping
+ called = []
+ executor.to_run { called << :run }
+ executor.to_complete { called << :complete }
+
+ executor.wrap do
+ called << :early
+ executor.wrap do
+ called << :body
+ end
+ called << :late
+ end
+
+ assert_equal [:run, :early, :body, :late, :complete], called
+ end
+
+ def test_hooks_carry_state
+ supplied_state = :none
+
+ hook = Class.new do
+ define_method(:run) do
+ :some_state
+ end
+
+ define_method(:complete) do |state|
+ supplied_state = state
+ end
+ end.new
+
+ executor.register_hook(hook)
+
+ executor.wrap { }
+
+ assert_equal :some_state, supplied_state
+ end
+
+ def test_nil_state_is_sufficient
+ supplied_state = :none
+
+ hook = Class.new do
+ define_method(:run) do
+ nil
+ end
+
+ define_method(:complete) do |state|
+ supplied_state = state
+ end
+ end.new
+
+ executor.register_hook(hook)
+
+ executor.wrap { }
+
+ assert_equal nil, supplied_state
+ end
+
+ def test_exception_skips_uninvoked_hook
+ supplied_state = :none
+
+ hook = Class.new do
+ define_method(:run) do
+ :some_state
+ end
+
+ define_method(:complete) do |state|
+ supplied_state = state
+ end
+ end.new
+
+ executor.to_run do
+ raise DummyError
+ end
+ executor.register_hook(hook)
+
+ assert_raises(DummyError) do
+ executor.wrap { }
+ end
+
+ assert_equal :none, supplied_state
+ end
+
+ def test_exception_unwinds_invoked_hook
+ supplied_state = :none
+
+ hook = Class.new do
+ define_method(:run) do
+ :some_state
+ end
+
+ define_method(:complete) do |state|
+ supplied_state = state
+ end
+ end.new
+
+ executor.register_hook(hook)
+ executor.to_run do
+ raise DummyError
+ end
+
+ assert_raises(DummyError) do
+ executor.wrap { }
+ end
+
+ assert_equal :some_state, supplied_state
+ end
+
+ def test_separate_classes_can_wrap
+ other_executor = Class.new(ActiveSupport::Executor)
+
+ called = []
+ executor.to_run { called << :run }
+ executor.to_complete { called << :complete }
+ other_executor.to_run { called << :other_run }
+ other_executor.to_complete { called << :other_complete }
+
+ executor.wrap do
+ other_executor.wrap do
+ called << :body
+ end
+ end
+
+ assert_equal [:run, :other_run, :body, :other_complete, :complete], called
+ end
+
+ private
+ def executor
+ @executor ||= Class.new(ActiveSupport::Executor)
+ end
+end
diff --git a/activesupport/test/file_update_checker_shared_tests.rb b/activesupport/test/file_update_checker_shared_tests.rb
index 9c07e38fe5..12e67a1e9f 100644
--- a/activesupport/test/file_update_checker_shared_tests.rb
+++ b/activesupport/test/file_update_checker_shared_tests.rb
@@ -5,7 +5,7 @@ module FileUpdateCheckerSharedTests
include FileUtils
def tmpdir
- @tmpdir ||= Dir.mktmpdir(nil, __dir__)
+ @tmpdir
end
def tmpfile(name)
@@ -16,8 +16,8 @@ module FileUpdateCheckerSharedTests
@tmpfiles ||= %w(foo.rb bar.rb baz.rb).map { |f| tmpfile(f) }
end
- def teardown
- FileUtils.rm_rf(@tmpdir) if defined? @tmpdir
+ def run(*args)
+ Dir.mktmpdir(nil, __dir__) { |dir| @tmpdir = dir; super }
end
test 'should not execute the block if no paths are given' do
@@ -134,6 +134,22 @@ module FileUpdateCheckerSharedTests
assert_equal 1, i
end
+ test 'should return max_time for files with mtime = Time.at(0)' do
+ i = 0
+
+ FileUtils.touch(tmpfiles)
+
+ time = Time.at(0) # wrong mtime from the future
+ File.utime(time, time, tmpfiles[0])
+
+ checker = new_checker(tmpfiles) { i += 1 }
+
+ touch(tmpfiles[1..-1])
+
+ assert checker.execute_if_updated
+ assert_equal 1, i
+ end
+
test 'should cache updated result until execute' do
i = 0
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index 9f4b62fd8b..5fc2e16336 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -422,6 +422,11 @@ EXPECTED
assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time)
end
+ def test_exception_to_json
+ exception = Exception.new("foo")
+ assert_equal '"foo"', ActiveSupport::JSON.encode(exception)
+ end
+
protected
def object_keys(json_object)
diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb
index 317e09b7f2..5a91420f1e 100644
--- a/activesupport/test/logger_test.rb
+++ b/activesupport/test/logger_test.rb
@@ -141,6 +141,50 @@ class LoggerTest < ActiveSupport::TestCase
assert @output.string.include?("THIS IS HERE")
end
+ def test_logger_silencing_works_for_broadcast
+ another_output = StringIO.new
+ another_logger = Logger.new(another_output)
+
+ @logger.extend Logger.broadcast(another_logger)
+
+ @logger.debug "CORRECT DEBUG"
+ @logger.silence do
+ @logger.debug "FAILURE"
+ @logger.error "CORRECT ERROR"
+ end
+
+ assert @output.string.include?("CORRECT DEBUG")
+ assert @output.string.include?("CORRECT ERROR")
+ assert_not @output.string.include?("FAILURE")
+
+ assert another_output.string.include?("CORRECT DEBUG")
+ assert another_output.string.include?("CORRECT ERROR")
+ assert_not another_output.string.include?("FAILURE")
+ end
+
+ def test_broadcast_silencing_does_not_break_plain_ruby_logger
+ another_output = StringIO.new
+ another_logger = ::Logger.new(another_output)
+
+ @logger.extend Logger.broadcast(another_logger)
+
+ @logger.debug "CORRECT DEBUG"
+ @logger.silence do
+ @logger.debug "FAILURE"
+ @logger.error "CORRECT ERROR"
+ end
+
+ assert @output.string.include?("CORRECT DEBUG")
+ assert @output.string.include?("CORRECT ERROR")
+ assert_not @output.string.include?("FAILURE")
+
+ assert another_output.string.include?("CORRECT DEBUG")
+ assert another_output.string.include?("CORRECT ERROR")
+ assert another_output.string.include?("FAILURE")
+ # We can't silence plain ruby Logger cause with thread safety
+ # but at least we don't break it
+ end
+
def test_logger_level_per_object_thread_safety
logger1 = Logger.new(StringIO.new)
logger2 = Logger.new(StringIO.new)
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index 6696111476..074c872efc 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -57,6 +57,8 @@ module ActiveSupport
assert_equal("+18005551212", number_helper.number_to_phone(8005551212, :country_code => 1, :delimiter => ''))
assert_equal("22-555-1212", number_helper.number_to_phone(225551212))
assert_equal("+45-22-555-1212", number_helper.number_to_phone(225551212, :country_code => 45))
+ assert_equal("(755) 6123-4567", number_helper.number_to_phone(75561234567, pattern: /(\d{3,4})(\d{4})(\d{4})/, area_code: true))
+ assert_equal("133-1234-5678", number_helper.number_to_phone(13312345678, pattern: /(\d{3})(\d{4})(\d{4})/))
end
end
diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb
new file mode 100644
index 0000000000..958cb49993
--- /dev/null
+++ b/activesupport/test/reloader_test.rb
@@ -0,0 +1,85 @@
+require 'abstract_unit'
+
+class ReloaderTest < ActiveSupport::TestCase
+ def test_prepare_callback
+ prepared = false
+ reloader.to_prepare { prepared = true }
+
+ assert !prepared
+ reloader.prepare!
+ assert prepared
+
+ prepared = false
+ reloader.wrap do
+ assert prepared
+ prepared = false
+ end
+ assert !prepared
+ end
+
+ def test_only_run_when_check_passes
+ r = new_reloader { true }
+ invoked = false
+ r.to_run { invoked = true }
+ r.wrap { }
+ assert invoked
+
+ r = new_reloader { false }
+ invoked = false
+ r.to_run { invoked = true }
+ r.wrap { }
+ assert !invoked
+ end
+
+ def test_full_reload_sequence
+ called = []
+ reloader.to_prepare { called << :prepare }
+ reloader.to_run { called << :reloader_run }
+ reloader.to_complete { called << :reloader_complete }
+ reloader.executor.to_run { called << :executor_run }
+ reloader.executor.to_complete { called << :executor_complete }
+
+ reloader.wrap { }
+ assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete], called
+
+ called = []
+ reloader.reload!
+ assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete, :prepare], called
+
+ reloader.check = lambda { false }
+
+ called = []
+ reloader.wrap { }
+ assert_equal [:executor_run, :executor_complete], called
+
+ called = []
+ reloader.reload!
+ assert_equal [:executor_run, :reloader_run, :prepare, :reloader_complete, :executor_complete, :prepare], called
+ end
+
+ def test_class_unload_block
+ called = []
+ reloader.before_class_unload { called << :before_unload }
+ reloader.after_class_unload { called << :after_unload }
+ reloader.to_run do
+ class_unload! do
+ called << :unload
+ end
+ end
+ reloader.wrap { called << :body }
+
+ assert_equal [:before_unload, :unload, :after_unload, :body], called
+ end
+
+ private
+ def new_reloader(&check)
+ Class.new(ActiveSupport::Reloader).tap do |r|
+ r.check = check
+ r.executor = Class.new(ActiveSupport::Executor)
+ end
+ end
+
+ def reloader
+ @reloader ||= new_reloader { true }
+ end
+end
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index d58016053b..4f4f27d6e4 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,3 +1,21 @@
+* Update example of passing a proc to `:message` option for validating records.
+
+ This behavior was recently changed in [Pull Request #24199](https://github.com/rails/rails/pull/24119) to
+ pass the object being validated as first argument to the `:message` proc,
+ instead of the key of the field being validated.
+
+ *Prathamesh Sonpatki*
+
+* Added new guide: Action Cable Overview.
+
+ *David Kuhta*
+
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* No changes.
+
+
## Rails 5.0.0.beta2 (February 01, 2016) ##
* No changes.
@@ -5,23 +23,23 @@
## Rails 5.0.0.beta1 (December 18, 2015) ##
-* Add code of conduct to contributing guide
+* Add code of conduct to contributing guide.
*Jon Moss*
-* New section in Configuring: Configuring Active Job
+* New section in Configuring: Configuring Active Job.
*Eliot Sykes*
-* New section in Active Record Association Basics: Single Table Inheritance
+* New section in Active Record Association Basics: Single Table Inheritance.
*Andrey Nering*
-* New section in Active Record Querying: Understanding The Method Chaining
+* New section in Active Record Querying: Understanding The Method Chaining.
*Andrey Nering*
-* New section in Configuring: Search Engines Indexing
+* New section in Configuring: Search Engines Indexing.
*Andrey Nering*
diff --git a/guides/assets/images/favicon.ico b/guides/assets/images/favicon.ico
index faa10b4580..87192a8a07 100644
--- a/guides/assets/images/favicon.ico
+++ b/guides/assets/images/favicon.ico
Binary files differ
diff --git a/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css b/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css
index 6d2edb2eb8..bc7afd3898 100644
--- a/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css
+++ b/guides/assets/stylesheets/syntaxhighlighter/shThemeRailsGuides.css
@@ -90,7 +90,7 @@
}
.syntaxhighlighter .script {
color: #222 !important;
- background-color: none !important;
+ background-color: transparent !important;
}
.syntaxhighlighter .color1, .syntaxhighlighter .color1 a {
color: gray !important;
diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb
index 3f24aa3b4d..8322707495 100644
--- a/guides/bug_report_templates/action_controller_master.rb
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -8,11 +8,6 @@ end
gemfile(true) do
source 'https://rubygems.org'
gem 'rails', github: 'rails/rails'
- gem 'arel', github: 'rails/arel'
- gem 'rack', github: 'rack/rack'
- gem 'sprockets', github: 'rails/sprockets'
- gem 'sprockets-rails', github: 'rails/sprockets-rails'
- gem 'sass-rails', github: 'rails/sass-rails'
end
require 'action_controller/railtie'
diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb
index 5b742a9093..a86edd9121 100644
--- a/guides/bug_report_templates/active_record_master.rb
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -8,11 +8,6 @@ end
gemfile(true) do
source 'https://rubygems.org'
gem 'rails', github: 'rails/rails'
- gem 'arel', github: 'rails/arel'
- gem 'rack', github: 'rack/rack'
- gem 'sprockets', github: 'rails/sprockets'
- gem 'sprockets-rails', github: 'rails/sprockets-rails'
- gem 'sass-rails', github: 'rails/sass-rails'
gem 'sqlite3'
end
diff --git a/guides/bug_report_templates/generic_master.rb b/guides/bug_report_templates/generic_master.rb
index fcc90fa503..70cf931f34 100644
--- a/guides/bug_report_templates/generic_master.rb
+++ b/guides/bug_report_templates/generic_master.rb
@@ -8,11 +8,6 @@ end
gemfile(true) do
source 'https://rubygems.org'
gem 'rails', github: 'rails/rails'
- gem 'arel', github: 'rails/arel'
- gem 'rack', github: 'rack/rack'
- gem 'sprockets', github: 'rails/sprockets'
- gem 'sprockets-rails', github: 'rails/sprockets-rails'
- gem 'sass-rails', github: 'rails/sass-rails'
end
require 'active_support'
diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md
index be00087f63..5e82af5a15 100644
--- a/guides/source/2_2_release_notes.md
+++ b/guides/source/2_2_release_notes.md
@@ -21,7 +21,7 @@ Rails 2.2 supplies an easy system for internationalization (or i18n, for those o
* Lead Contributors: Rails i18 Team
* More information :
* [Official Rails i18 website](http://rails-i18n.org)
- * [Finally. Ruby on Rails gets internationalized](http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized)
+ * [Finally. Ruby on Rails gets internationalized](https://web.archive.org/web/20140407075019/http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized)
* [Localizing Rails : Demo application](http://github.com/clemens/i18n_demo_app)
### Compatibility with Ruby 1.9 and JRuby
diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md
index 327495704a..feee0f9920 100644
--- a/guides/source/3_1_release_notes.md
+++ b/guides/source/3_1_release_notes.md
@@ -558,4 +558,4 @@ Credits
See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them.
-Rails 3.1 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev.)
+Rails 3.1 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev)
diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md
index b9444510ea..4615cf18e6 100644
--- a/guides/source/4_0_release_notes.md
+++ b/guides/source/4_0_release_notes.md
@@ -108,7 +108,7 @@ In Rails 4.0, several features have been extracted into gems. You can simply add
* Mass assignment protection in Active Record models ([GitHub](https://github.com/rails/protected_attributes), [Pull Request](https://github.com/rails/rails/pull/7251))
* ActiveRecord::SessionStore ([GitHub](https://github.com/rails/activerecord-session_store), [Pull Request](https://github.com/rails/rails/pull/7436))
* Active Record Observers ([GitHub](https://github.com/rails/rails-observers), [Commit](https://github.com/rails/rails/commit/39e85b3b90c58449164673909a6f1893cba290b2))
-* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource))
+* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia-blog-blog.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource))
* Action Caching ([GitHub](https://github.com/rails/actionpack-action_caching), [Pull Request](https://github.com/rails/rails/pull/7833))
* Page Caching ([GitHub](https://github.com/rails/actionpack-page_caching), [Pull Request](https://github.com/rails/rails/pull/7833))
* Sprockets ([GitHub](https://github.com/rails/sprockets-rails))
diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md
index 8a59007420..73e6c2c05b 100644
--- a/guides/source/4_2_release_notes.md
+++ b/guides/source/4_2_release_notes.md
@@ -405,7 +405,7 @@ Please refer to the [Changelog][railties] for detailed changes.
url: http://localhost:3001
namespace: my_app_development
- # config/production.rb
+ # config/environments/production.rb
Rails.application.configure do
config.middleware.use ExceptionNotifier, config_for(:exception_notification)
end
diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md
index 4e8252f85b..28f653b634 100644
--- a/guides/source/5_0_release_notes.md
+++ b/guides/source/5_0_release_notes.md
@@ -94,6 +94,10 @@ Please refer to the [Changelog][railties] for detailed changes.
* Deprecated `config.serve_static_files` in favor of `config.public_file_server.enabled`.
([Pull Request](https://github.com/rails/rails/pull/22173))
+* Deprecated the tasks in the `rails` task namespace in favor of the `app` namespace.
+ (e.g. `rails:update` and `rails:template` tasks is renamed to `app:update` and `app:template`.)
+ ([Pull Request](https://github.com/rails/rails/pull/23439))
+
### Notable changes
* Added Rails test runner `bin/rails test`.
@@ -120,6 +124,23 @@ Please refer to the [Changelog][railties] for detailed changes.
([Pull Request](https://github.com/rails/rails/pull/22457),
[Pull Request](https://github.com/rails/rails/pull/22288))
+* New applications are generated with the evented file system monitor enabled
+ on Linux and Mac OS X. The feature can be opted out by passing
+ `--skip-listen` to the generator.
+ ([commit](https://github.com/rails/rails/commit/de6ad5665d2679944a9ee9407826ba88395a1003),
+ [commit](https://github.com/rails/rails/commit/94dbc48887bf39c241ee2ce1741ee680d773f202))
+
+* Generate applications with an option to log to STDOUT in production
+ using the environment variable `RAILS_LOG_TO_STDOUT`.
+ ([Pull Request](https://github.com/rails/rails/pull/23734))
+
+* Enable HSTS with IncludeSudomains header for new applications.
+ ([Pull Request](https://github.com/rails/rails/pull/23852))
+
+* The application generator writes a new file `config/spring.rb`, which tells
+ Spring to watch additional common files.
+ ([commit](https://github.com/rails/rails/commit/b04d07337fd7bc17e88500e9d6bcd361885a45f8))
+
Action Pack
-----------
@@ -249,13 +270,30 @@ Please refer to the [Changelog][action-pack] for detailed changes.
* Changed the `protect_from_forgery` prepend default to `false`.
([commit](https://github.com/rails/rails/commit/39794037817703575c35a75f1961b01b83791191))
-* `ActionController::TestCase` will be moved to it's own gem in Rails 5.1. Use
+* `ActionController::TestCase` will be moved to its own gem in Rails 5.1. Use
`ActionDispatch::IntegrationTest` instead.
([commit](https://github.com/rails/rails/commit/4414c5d1795e815b102571425974a8b1d46d932d))
* Rails will only generate "weak", instead of strong ETags.
([Pull Request](https://github.com/rails/rails/pull/17573))
+* Controller actions without an explicit `render` call and with no
+ corresponding templates will render `head :no_content` implicitly
+ instead of raising an error.
+ (Pull Request [1](https://github.com/rails/rails/pull/19377),
+ [2](https://github.com/rails/rails/pull/23827))
+
+* Added an option for per-form CSRF tokens.
+ ([Pull Request](https://github.com/rails/rails/pull/22275))
+
+* Added request encoding and response parsing to integration tests.
+ ([Pull Request](https://github.com/rails/rails/pull/21671))
+
+* Update default rendering policies when the controller action did
+ not explicitly indicate a response.
+ ([Pull Request](https://github.com/rails/rails/pull/23827))
+
+
Action View
-------------
@@ -280,12 +318,9 @@ Please refer to the [Changelog][action-view] for detailed changes.
* Changed the default template handler from `ERB` to `Raw`.
([commit](https://github.com/rails/rails/commit/4be859f0fdf7b3059a28d03c279f03f5938efc80))
-* Collection rendering automatically caches and fetches multiple partials.
- ([Pull Request](https://github.com/rails/rails/pull/18948))
-
-* Allow defining explicit collection caching using a `# Template Collection: ...`
- directive inside templates.
- ([Pull Request](https://github.com/rails/rails/pull/20781))
+* Collection rendering can cache and fetches multiple partials.
+ ([Pull Request](https://github.com/rails/rails/pull/18948),
+ [commit](https://github.com/rails/rails/commit/e93f0f0f133717f9b06b1eaefd3442bd0ff43985))
* Added wildcard matching to explicit dependencies.
([Pull Request](https://github.com/rails/rails/pull/20904))
@@ -294,6 +329,9 @@ Please refer to the [Changelog][action-view] for detailed changes.
button on submit to prevent double submits.
([Pull Request](https://github.com/rails/rails/pull/21135))
+* Collection rendering can cache and fetch multiple partials at once.
+ ([Pull Request](https://github.com/rails/rails/pull/21135))
+
Action Mailer
-------------
@@ -324,6 +362,11 @@ Please refer to the [Changelog][action-mailer] for detailed changes.
the mailer queue name.
([Pull Request](https://github.com/rails/rails/pull/18587))
+* Added support for fragment caching in Action Mailer views.
+ Added new config option `config.action_mailer.perform_caching` to determine
+ whether your templates should perform caching or not.
+ ([Pull Request](https://github.com/rails/rails/pull/22825))
+
Active Record
-------------
@@ -396,10 +439,6 @@ Please refer to the [Changelog][active-record] for detailed changes.
* Deprecated `ActiveRecord::Base.errors_in_transactional_callbacks=`.
([commit](https://github.com/rails/rails/commit/07d3d402341e81ada0214f2cb2be1da69eadfe72))
-* Deprecated passing of `start` value to `find_in_batches` and `find_each`
- in favour of `begin_at` value.
- ([Pull Request](https://github.com/rails/rails/pull/18961))
-
* Deprecated `Relation#uniq` use `Relation#distinct` instead.
([commit](https://github.com/rails/rails/commit/adfab2dcf4003ca564d78d4425566dd2d9cd8b4f))
@@ -441,6 +480,10 @@ Please refer to the [Changelog][active-record] for detailed changes.
`offset` method on relation instead.
([Pull Request](https://github.com/rails/rails/pull/22053))
+* Deprecated `{insert|update|delete}_sql` in `DatabaseStatements`.
+ Use the `{insert|update|delete}` public methods instead.
+ ([Pull Request](https://github.com/rails/rails/pull/23086))
+
### Notable changes
* Added a `foreign_key` option to `references` while creating the table.
@@ -455,9 +498,6 @@ Please refer to the [Changelog][active-record] for detailed changes.
* Added `#cache_key` to `ActiveRecord::Relation`.
([Pull Request](https://github.com/rails/rails/pull/20884))
-* Added `ActiveRecord::Relation#outer_joins`.
- ([Pull Request](https://github.com/rails/rails/pull/12071))
-
* Require `belongs_to` by default.
([Pull Request](https://github.com/rails/rails/pull/18937)) - Deprecate
`required` option in favor of `optional` for `belongs_to`
@@ -542,6 +582,12 @@ Please refer to the [Changelog][active-record] for detailed changes.
model behavior.
([Pull Request](https://github.com/rails/rails/pull/22567))
+* Added ActiveRecord `#second_to_last` and `#third_to_last` methods.
+ ([Pull Request](https://github.com/rails/rails/pull/23583))
+
+* Added ability to annotate database objects (tables, columns, indexes)
+ with comments stored in database metadata for PostgreSQL & MySQL.
+ ([Pull Request](https://github.com/rails/rails/pull/22911))
Active Model
------------
@@ -609,15 +655,20 @@ Please refer to the [Changelog][active-job] for detailed changes.
* A generated job now inherits from `app/jobs/application_job.rb` by default.
([Pull Request](https://github.com/rails/rails/pull/19034))
-* Allow `DelayedJob`, `Sidekiq`, `qu`, and `que` to report the job id back to
- `ActiveJob::Base` as `provider_job_id`.
+* Allow `DelayedJob`, `Sidekiq`, `qu`, `que`, and `queue_classic` to report
+ the job id back to `ActiveJob::Base` as `provider_job_id`.
([Pull Request](https://github.com/rails/rails/pull/20064),
- [Pull Request](https://github.com/rails/rails/pull/20056))
+ [Pull Request](https://github.com/rails/rails/pull/20056),
+ [commit](https://github.com/rails/rails/commit/68e3279163d06e6b04e043f91c9470e9259bbbe0))
* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that
queue jobs to a `concurrent-ruby` thread pool.
([Pull Request](https://github.com/rails/rails/pull/21257))
+* Change the default adapter from inline to async. It's a better default as
+ tests will then not mistakenly come to rely on behavior happening
+ synchronously.
+ ([commit](https://github.com/rails/rails/commit/625baa69d14881ac49ba2e5c7d9cac4b222d7022))
Active Support
--------------
@@ -686,6 +737,13 @@ Please refer to the [Changelog][active-support] for detailed changes.
Deprecated `ActiveSupport::Cache::LocaleCache#set_cache_value` in favor of `write_cache_value`.
([Pull Request](https://github.com/rails/rails/pull/22215))
+* Deprecated passing arguments to `assert_nothing_raised`.
+ ([Pull Request](https://github.com/rails/rails/pull/23789))
+
+* Deprecated `Module.local_constants` in favor of `Module.constants(false)`.
+ ([Pull Request](https://github.com/rails/rails/pull/23936))
+
+
### Notable changes
* Added `#verified` and `#valid_message?` methods to
@@ -705,7 +763,7 @@ Please refer to the [Changelog][active-support] for detailed changes.
* Changed the default test order from `:sorted` to `:random`.
([commit](https://github.com/rails/rails/commit/5f777e4b5ee2e3e8e6fd0e2a208ec2a4d25a960d))
-* Added `#on_weekend?`, `#next_weekday`, `#prev_weekday` methods to `Date`,
+* Added `#on_weekend?`, `#on_weekday?`, `#next_weekday`, `#prev_weekday` methods to `Date`,
`Time`, and `DateTime`.
([Pull Request](https://github.com/rails/rails/pull/18335))
@@ -755,6 +813,20 @@ Please refer to the [Changelog][active-support] for detailed changes.
class and module variables that live per-thread.
([Pull Request](https://github.com/rails/rails/pull/22630))
+* Added `Array#second_to_last` and `Array#third_to_last` methods.
+ ([Pull Request](https://github.com/rails/rails/pull/23583))
+
+* Added `#on_weekday?` method to `Date`, `Time`, and `DateTime`.
+ ([Pull Request](https://github.com/rails/rails/pull/23687))
+
+* Publish `ActiveSupport::Executor` and `ActiveSupport::Reloader` APIs to allow
+ components and libraries to manage, and participate in, the execution of
+ application code, and the application reloading process.
+ ([Pull Request](https://github.com/rails/rails/pull/23807))
+
+* `ActiveSupport::Duration` now supports ISO8601 formatting and parsing.
+ ([Pull Request](https://github.com/rails/rails/pull/16917))
+
Credits
-------
@@ -768,6 +840,7 @@ framework it is. Kudos to all of them.
[action-pack]: https://github.com/rails/rails/blob/5-0-stable/actionpack/CHANGELOG.md
[action-view]: https://github.com/rails/rails/blob/5-0-stable/actionview/CHANGELOG.md
[action-mailer]: https://github.com/rails/rails/blob/5-0-stable/actionmailer/CHANGELOG.md
+[action-cable]: https://github.com/rails/rails/blob/5-0-stable/actioncable/CHANGELOG.md
[active-record]: https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md
[active-model]: https://github.com/rails/rails/blob/5-0-stable/activemodel/CHANGELOG.md
[active-support]: https://github.com/rails/rails/blob/5-0-stable/activesupport/CHANGELOG.md
diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md
new file mode 100644
index 0000000000..0c486bb96c
--- /dev/null
+++ b/guides/source/action_cable_overview.md
@@ -0,0 +1,621 @@
+Action Cable Overview
+=====================
+
+In this guide you will learn how Action Cable works and how to use WebSockets to
+incorporate real-time features into your Rails application.
+
+After reading this guide, you will know:
+
+* How to setup Action Cable
+* How to setup channels
+
+Introduction
+------------
+
+Action Cable seamlessly integrates WebSockets with the rest of your Rails application.
+It allows for real-time features to be written in Ruby in the same style and form as
+the rest of your Rails application, while still being performant and scalable. It's
+a full-stack offering that provides both a client-side JavaScript framework and a
+server-side Ruby framework. You have access to your full domain model written with
+Active Record or your ORM of choice.
+
+What is Pub/Sub
+---------------
+
+Pub/Sub, or Publish-Subscribe, refers to a message queue paradigm whereby senders
+of information (publishers), send data to an abstract class of recipients (subscribers),
+without specifying individual recipients. Action Cable uses this approach to communicate
+between the server and many clients.
+
+What is Action Cable
+--------------------
+
+Action Cable is a server which can handle multiple connection instances, with one
+client-server connection instance established per WebSocket connection.
+
+## Server-Side Components
+
+### Connections
+
+Connections form the foundation of the client-server relationship. For every WebSocket
+the cable server is accepting, a Connection object will be instantiated on the server side.
+This instance becomes the parent of all the channel subscriptions that are created from there on.
+The Connection itself does not deal with any specific application logic beyond authentication
+and authorization. The client of a WebSocket connection is called a consumer. An individual
+user will create one consumer-connection pair per browser tab, window, or device they have open.
+
+Connections are instantiated via the `ApplicationCable::Connection` class in Ruby.
+In this class, you authorize the incoming connection, and proceed to establish it
+if the user can be identified.
+
+#### Connection Setup
+
+```ruby
+# app/channels/application_cable/connection.rb
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ identified_by :current_user
+
+ def connect
+ self.current_user = find_verified_user
+ end
+
+ protected
+ def find_verified_user
+ if current_user = User.find_by(id: cookies.signed[:user_id])
+ current_user
+ else
+ reject_unauthorized_connection
+ end
+ end
+ end
+end
+```
+
+Here `identified_by` is a connection identifier that can be used to find the
+specific connection later. Note that anything marked as an identifier will automatically
+create a delegate by the same name on any channel instances created off the connection.
+
+This example relies on the fact that you will already have handled authentication of the user
+somewhere else in your application, and that a successful authentication sets a signed
+cookie with the `user_id`.
+
+The cookie is then automatically sent to the connection instance when a new connection
+is attempted, and you use that to set the `current_user`. By identifying the connection
+by this same current_user, you're also ensuring that you can later retrieve all open
+connections by a given user (and potentially disconnect them all if the user is deleted
+or deauthorized).
+
+### Channels
+
+A channel encapsulates a logical unit of work, similar to what a controller does in a
+regular MVC setup. By default, Rails creates a parent `ApplicationCable::Channel` class
+for encapsulating shared logic between your channels.
+
+#### Parent Channel Setup
+
+```ruby
+# app/channels/application_cable/channel.rb
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
+```
+
+Then you would create your own channel classes. For example, you could have a
+**ChatChannel** and an **AppearanceChannel**:
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+end
+
+# app/channels/appearance_channel.rb
+class AppearanceChannel < ApplicationCable::Channel
+end
+```
+
+A consumer could then be subscribed to either or both of these channels.
+
+#### Subscriptions
+
+When a consumer is subscribed to a channel, they act as a subscriber;
+This connection is called a subscription.
+Incoming messages are then routed to these channel subscriptions based on
+an identifier sent by the cable consumer.
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ # Called when the consumer has successfully become a subscriber of this channel
+ def subscribed
+ end
+end
+```
+
+## Client-Side Components
+
+### Connections
+
+Consumers require an instance of the connection on their side. This can be
+established using the following Javascript, which is generated by default in Rails:
+
+#### Connect Consumer
+
+```coffeescript
+# app/assets/javascripts/cable.coffee
+#= require action_cable
+
+@App = {}
+App.cable = ActionCable.createConsumer()
+```
+
+This will ready a consumer that'll connect against /cable on your server by default.
+The connection won't be established until you've also specified at least one subscription
+you're interested in having.
+
+#### Subscriber
+
+When a consumer is subscribed to a channel, they act as a subscriber. A
+consumer can act as a subscriber to a given channel any number of times.
+For example, a consumer could subscribe to multiple chat rooms at the same time.
+(remember that a physical user may have multiple consumers, one per tab/device open to your connection).
+
+A consumer becomes a subscriber, by creating a subscription to a given channel:
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }
+
+# app/assets/javascripts/cable/subscriptions/appearance.coffee
+App.cable.subscriptions.create { channel: "AppearanceChannel" }
+```
+
+While this creates the subscription, the functionality needed to respond to
+received data will be described later on.
+
+## Client-Server Interactions
+
+### Streams
+
+Streams provide the mechanism by which channels route published content
+(broadcasts) to its subscribers.
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "chat_#{params[:room]}"
+ end
+end
+```
+
+If you have a stream that is related to a model, then the broadcasting used
+can be generated from the model and channel. The following example would
+subscribe to a broadcasting like `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE`
+
+```ruby
+class CommentsChannel < ApplicationCable::Channel
+ def subscribed
+ post = Post.find(params[:id])
+ stream_for post
+ end
+end
+```
+
+You can then broadcast to this channel using: `CommentsChannel.broadcast_to(@post, @comment)`
+
+### Broadcastings
+
+A broadcasting is a pub/sub link where anything transmitted by a publisher
+is routed directly to the channel subscribers who are streaming that named
+broadcasting. Each channel can be streaming zero or more broadcastings.
+Broadcastings are purely an online queue and time dependent;
+If a consumer is not streaming (subscribed to a given channel), they'll not
+get the broadcast should they connect later.
+
+Broadcasts are called elsewhere in your Rails application:
+```ruby
+ WebNotificationsChannel.broadcast_to current_user, title: 'New things!', body: 'All the news fit to print'
+```
+
+The `WebNotificationsChannel.broadcast_to` call places a message in the current
+subscription adapter (Redis by default)'s pubsub queue under a separate
+broadcasting name for each user. For a user with an ID of 1, the broadcasting
+name would be `web_notifications_1`.
+
+The channel has been instructed to stream everything that arrives at
+`web_notifications_1` directly to the client by invoking the `#received(data)`
+callback.
+
+### Subscriptions
+
+When a consumer is subscribed to a channel, they act as a subscriber;
+This connection is called a subscription. Incoming messages are then routed
+to these channel subscriptions based on an identifier sent by the cable consumer.
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+# Assumes you've already requested the right to send web notifications
+App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
+ received: (data) ->
+ @appendLine(data)
+
+ appendLine: (data) ->
+ html = @createLine(data)
+ $("[data-chat-room='Best Room']").append(html)
+
+ createLine: (data) ->
+ """
+ <article class="chat-line">
+ <span class="speaker">#{data["sent_by"]}</span>
+ <span class="body">#{data["body"]}</span>
+ </article>
+ """
+```
+
+### Passing Parameters to Channel
+
+You can pass parameters from the client-side to the server-side when
+creating a subscription. For example:
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "chat_#{params[:room]}"
+ end
+end
+```
+
+Pass an object as the first argument to `subscriptions.create`, and that object
+will become your params hash in your cable channel. The keyword `channel` is required.
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
+ received: (data) ->
+ @appendLine(data)
+
+ appendLine: (data) ->
+ html = @createLine(data)
+ $("[data-chat-room='Best Room']").append(html)
+
+ createLine: (data) ->
+ """
+ <article class="chat-line">
+ <span class="speaker">#{data["sent_by"]}</span>
+ <span class="body">#{data["body"]}</span>
+ </article>
+ """
+```
+
+```ruby
+# Somewhere in your app this is called, perhaps from a NewCommentJob
+ChatChannel.broadcast_to "chat_#{room}", sent_by: 'Paul', body: 'This is a cool chat app.'
+```
+
+
+### Rebroadcasting message
+
+A common use case is to rebroadcast a message sent by one client to any
+other connected clients.
+
+```ruby
+# app/channels/chat_channel.rb
+class ChatChannel < ApplicationCable::Channel
+ def subscribed
+ stream_from "chat_#{params[:room]}"
+ end
+
+ def receive(data)
+ ChatChannel.broadcast_to "chat_#{params[:room]}", data
+ end
+end
+```
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/chat.coffee
+App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
+ received: (data) ->
+ # data => { sent_by: "Paul", body: "This is a cool chat app." }
+
+App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
+```
+
+The rebroadcast will be received by all connected clients, _including_ the
+client that sent the message. Note that params are the same as they were when
+you subscribed to the channel.
+
+## Full-stack examples
+
+The following setup steps are common to both examples:
+
+ 1. [Setup your connection](#connection-setup)
+ 2. [Setup your parent channel](#parent-channel-setup)
+ 3. [Connect your consumer](#connect-consumer)
+
+### Example 1: User appearances
+Here's a simple example of a channel that tracks whether a user is online or not
+and what page they're on. (This is useful for creating presence features like showing
+a green dot next to a user name if they're online).
+
+#### Create the server-side Appearance Channel:
+
+```ruby
+# app/channels/appearance_channel.rb
+class AppearanceChannel < ApplicationCable::Channel
+ def subscribed
+ current_user.appear
+ end
+
+ def unsubscribed
+ current_user.disappear
+ end
+
+ def appear(data)
+ current_user.appear on: data['appearing_on']
+ end
+
+ def away
+ current_user.away
+ end
+end
+```
+
+When `#subscribed` callback is invoked by the consumer, a client-side subscription
+is initiated. In this case, we take that opportunity to say "the current user has
+indeed appeared". That appear/disappear API could be backed by Redis, a database,
+or whatever else.
+
+#### Create the client-side Appearance Channel subscription:
+
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/appearance.coffee
+App.cable.subscriptions.create "AppearanceChannel",
+ # Called when the subscription is ready for use on the server
+ connected: ->
+ @install()
+ @appear()
+
+ # Called when the WebSocket connection is closed
+ disconnected: ->
+ @uninstall()
+
+ # Called when the subscription is rejected by the server
+ rejected: ->
+ @uninstall()
+
+ appear: ->
+ # Calls `AppearanceChannel#appear(data)` on the server
+ @perform("appear", appearing_on: $("main").data("appearing-on"))
+
+ away: ->
+ # Calls `AppearanceChannel#away` on the server
+ @perform("away")
+
+
+ buttonSelector = "[data-behavior~=appear_away]"
+
+ install: ->
+ $(document).on "page:change.appearance", =>
+ @appear()
+
+ $(document).on "click.appearance", buttonSelector, =>
+ @away()
+ false
+
+ $(buttonSelector).show()
+
+ uninstall: ->
+ $(document).off(".appearance")
+ $(buttonSelector).hide()
+```
+
+##### Client-Server Interaction
+1. **Client** establishes a connection with the **Server** via `App.cable = ActionCable.createConsumer("ws://cable.example.com")`. [*` cable.coffee`*] The **Server** identified this connection instance by `current_user`.
+2. **Client** initiates a subscription to the `Appearance Channel` for their connection via `App.cable.subscriptions.create "AppearanceChannel"`. [*`appearance.coffee`*]
+3. **Server** recognizes a new subscription has been initiated for `AppearanceChannel` channel performs the `subscribed` callback, which calls the `appear` method on the `current_user`. [*`appearance_channel.rb`*]
+4. **Client** recognizes that a subscription has been established and calls `connected` [*`appearance.coffee`*] which in turn calls `@install` and `@appear`. `@appear` calls`AppearanceChannel#appear(data)` on the server, and supplies a data hash of `appearing_on: $("main").data("appearing-on")`. This is possible because the server-side channel instance will automatically expose the public methods declared on the class (minus the callbacks), so that these can be reached as remote procedure calls via a subscription's `perform` method.
+5. **Server** receives the request for the `appear` action on the `AppearanceChannel` channel for the connection identified by `current_user`. [*`appearance_channel.rb`*] The server retrieves the data with the `appearing_on` key from the data hash and sets it as the value for the `on:` key being passed to `current_user.appear`.
+
+### Example 2: Receiving new web notifications
+
+The appearance example was all about exposing server functionality to
+client-side invocation over the WebSocket connection. But the great thing
+about WebSockets is that it's a two-way street. So now let's show an example
+where the server invokes an action on the client.
+
+This is a web notification channel that allows you to trigger client-side
+web notifications when you broadcast to the right streams:
+
+#### Create the server-side Web Notifications Channel:
+
+```ruby
+# app/channels/web_notifications_channel.rb
+class WebNotificationsChannel < ApplicationCable::Channel
+ def subscribed
+ stream_for current_user
+ end
+end
+```
+
+#### Create the client-side Web Notifications Channel subscription:
+```coffeescript
+# app/assets/javascripts/cable/subscriptions/web_notifications.coffee
+# Client-side which assumes you've already requested the right to send web notifications
+App.cable.subscriptions.create "WebNotificationsChannel",
+ received: (data) ->
+ new Notification data["title"], body: data["body"]
+```
+
+#### Broadcast content to a Web Notification Channel instance from elsewhere in your application
+
+```ruby
+# Somewhere in your app this is called, perhaps from a NewCommentJob
+ WebNotificationsChannel.broadcast_to current_user, title: 'New things!', body: 'All the news fit to print'
+```
+
+The `WebNotificationsChannel.broadcast_to` call places a message in the current
+subscription adapter (Redis by default)'s pubsub queue under a separate
+broadcasting name for each user. For a user with an ID of 1, the broadcasting
+name would be `web_notifications_1`.
+
+The channel has been instructed to stream everything that arrives at
+`web_notifications_1` directly to the client by invoking the `#received(data)`
+callback. The data is the hash sent as the second parameter to the server-side
+broadcast call, JSON encoded for the trip across the wire, and unpacked for
+the data argument arriving to `#received`.
+
+### More complete examples
+
+See the [rails/actioncable-examples](http://github.com/rails/actioncable-examples)
+repository for a full example of how to setup Action Cable in a Rails app and adding channels.
+
+## Configuration
+
+Action Cable has two required configurations: a subscription adapter and allowed request origins.
+
+### Subscription Adapter
+
+By default, `ActionCable::Server::Base` will look for a configuration file
+in `Rails.root.join('config/cable.yml')`. The file must specify an adapter
+and a URL for each Rails environment. See the "Dependencies" section for
+additional information on adapters.
+
+```yaml
+production: &production
+ adapter: redis
+ url: redis://10.10.3.153:6381
+development: &development
+ adapter: async
+test: *development
+```
+
+This format allows you to specify one configuration per Rails environment.
+You can also change the location of the Action Cable config file in
+a Rails initializer with something like:
+
+```ruby
+Rails.application.paths.add "config/redis/cable", with: "somewhere/else/cable.yml"
+```
+
+### Allowed Request Origins
+
+Action Cable will only accept requests from specified origins, which are
+passed to the server config as an array. The origins can be instances of
+strings or regular expressions, against which a check for match will be performed.
+
+```ruby
+Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]
+```
+
+To disable and allow requests from any origin:
+
+```ruby
+Rails.application.config.action_cable.disable_request_forgery_protection = true
+```
+
+By default, Action Cable allows all requests from localhost:3000 when running
+in the development environment.
+
+
+### Consumer Configuration
+
+To configure the URL, add a call to `action_cable_meta_tag` in your HTML layout HEAD.
+This uses a url or path typically set via `config.action_cable.url` in the environment configuration files.
+
+### Other Configurations
+
+The other common option to configure is the log tags applied to the per-connection logger. Here's close to what we're using in Basecamp:
+
+```ruby
+Rails.application.config.action_cable.log_tags = [
+ -> request { request.env['bc.account_id'] || "no-account" },
+ :action_cable,
+ -> request { request.uuid }
+]
+```
+
+For a full list of all configuration options, see the `ActionCable::Server::Configuration` class.
+
+Also note that your server must provide at least the same number of
+database connections as you have workers. The default worker pool is
+set to 100, so that means you have to make at least that available.
+You can change that in `config/database.yml` through the `pool` attribute.
+
+## Running standalone cable servers
+
+### In App
+
+Action Cable can run alongside your Rails application. For example, to
+listen for WebSocket requests on `/websocket`, specify that path to
+`config.action_cable.mount_path`:
+
+```ruby
+# config/application.rb
+class Application < Rails::Application
+ config.action_cable.mount_path = '/websocket'
+end
+```
+
+You can use `App.cable = ActionCable.createConsumer()` to connect to the
+cable server if `action_cable_meta_tag` is included in the layout. A custom
+path is specified as first argument to `createConsumer`
+(e.g. `App.cable = ActionCable.createConsumer("/websocket")`).
+
+For every instance of your server you create and for every worker
+your server spawns, you will also have a new instance of ActionCable,
+but the use of Redis keeps messages synced across connections.
+
+### Standalone
+
+The cable servers can be separated from your normal application server.
+It's still a Rack application, but it is its own Rack application.
+The recommended basic setup is as follows:
+
+```ruby
+# cable/config.ru
+require ::File.expand_path('../../config/environment', __FILE__)
+Rails.application.eager_load!
+
+run ActionCable.server
+```
+
+Then you start the server using a binstub in bin/cable ala:
+
+```
+#!/bin/bash
+bundle exec puma -p 28080 cable/config.ru
+```
+
+The above will start a cable server on port 28080.
+
+### Notes
+
+The WebSocket server doesn't have access to the session, but it has
+access to the cookies. This can be used when you need to handle
+authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication).
+
+## Dependencies
+
+Action Cable provides a subscription adapter interface to process its
+pubsub internals. By default, asynchronous, inline, PostgreSQL, evented
+Redis, and non-evented Redis adapters are included. The default adapter
+in new Rails applications is the asynchronous (`async`) adapter.
+
+The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby),
+[nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby).
+
+## Deployment
+
+Action Cable is powered by a combination of WebSockets and threads. Both the
+framework plumbing and user-specified channel work are handled internally by
+utilizing Ruby's native thread support. This means you can use all your regular
+Rails models with no problem, as long as you haven't committed any thread-safety sins.
+
+The Action Cable server implements the Rack socket hijacking API,
+thereby allowing the use of a multithreaded pattern for managing connections
+internally, irrespective of whether the application server is multi-threaded or not.
+
+Accordingly, Action Cable works with all the popular application servers -- Unicorn, Puma and Passenger.
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 9ef2c1a441..848c9caa59 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -700,7 +700,7 @@ class LoginsController < ApplicationController
end
```
-Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to only skip this filter for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place.
+Now, the `LoginsController`'s `new` and `create` actions will work as before without requiring the user to be logged in. The `:only` option is used to skip this filter only for these actions, and there is also an `:except` option which works the other way. These options can be used when adding filters too, so you can add a filter which only runs for selected actions in the first place.
### After Filters and Around Filters
@@ -1088,6 +1088,8 @@ You can filter out sensitive request parameters from your log files by appending
config.filter_parameters << :password
```
+NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
+
### Redirects Filtering
Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to.
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index cd2c13e8c1..5346b7c32b 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -204,10 +204,14 @@ class UsersController < ApplicationController
end
```
-NOTE: Active Job's default behavior is to execute jobs ':inline'. So, you can use
-`deliver_later` now to send emails, and when you later decide to start sending
-them from a background job, you'll only need to set up Active Job to use a queueing
-backend (Sidekiq, Resque, etc).
+NOTE: Active Job's default behavior is to execute jobs via the `:async` adapter. So, you can use
+`deliver_later` now to send emails asynchronously.
+Active Job's default adapter runs jobs with an in-process thread pool.
+It's well-suited for the development/test environments, since it doesn't require
+any external infrastructure, but it's a poor fit for production since it drops
+pending jobs on restart.
+If you need a persistent backend, you will need to use an Active Job adapter
+that has a persistent backend (Sidekiq, Resque, etc).
If you want to send emails right away (from a cronjob for example) just call
`deliver_now`:
@@ -222,7 +226,7 @@ class SendWeeklySummary
end
```
-The method `welcome_email` returns a `ActionMailer::MessageDelivery` object which
+The method `welcome_email` returns an `ActionMailer::MessageDelivery` object which
can then just be told `deliver_now` or `deliver_later` to send itself out. The
`ActionMailer::MessageDelivery` object is just a wrapper around a `Mail::Message`. If
you want to inspect, alter or do anything else with the `Mail::Message` object you can
@@ -278,7 +282,7 @@ different, encode your content and pass in the encoded content and encoding in a
```ruby
encoded_content = SpecialEncode(File.read('/path/to/filename.jpg'))
attachments['filename.jpg'] = {
- mime_type: 'application/x-gzip',
+ mime_type: 'application/gzip',
encoding: 'SpecialEncoding',
content: encoded_content
}
@@ -407,6 +411,22 @@ use the rendered text for the text part. The render command is the same one used
inside of Action Controller, so you can use all the same options, such as
`:text`, `:inline` etc.
+#### Caching mailer view
+
+You can do cache in mailer views like in application views using `cache` method.
+
+```
+<% cache do %>
+ <%= @company.name %>
+<% end %>
+```
+
+And in order to use this feature, you need to configure your application with this:
+
+```
+ config.action_mailer.perform_caching = true
+```
+
### Action Mailer Layouts
Just like controller views, you can also have mailer layouts. The layout name
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index 543937f8e5..0e6bb76101 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -173,11 +173,11 @@ would produce:
```json
{
"name": "Alex",
- "email: "alex@example.com"
+ "email": "alex@example.com"
}
```
-See the [Jbuilder documention](https://github.com/rails/jbuilder#jbuilder) for
+See the [Jbuilder documentation](https://github.com/rails/jbuilder#jbuilder) for
more examples and information.
#### Template Caching
@@ -260,7 +260,7 @@ With the `as` option we can specify a different name for the local variable. For
<%= render partial: "product", as: "item" %>
```
-The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (eg. in a different instance variable or in a local variable).
+The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (e.g. in a different instance variable or in a local variable).
For example, instead of:
@@ -442,7 +442,7 @@ image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png
#### image_url
-Computes the url to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host.
+Computes the URL to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host.
```ruby
image_url("edit.png") # => http://www.example.com/assets/edit.png
@@ -493,7 +493,7 @@ javascript_path "common" # => /assets/common.js
#### javascript_url
-Computes the url to a JavaScript asset in the `app/assets/javascripts` directory. This will call `javascript_path` internally and merge with your current host or your asset host.
+Computes the URL to a JavaScript asset in the `app/assets/javascripts` directory. This will call `javascript_path` internally and merge with your current host or your asset host.
```ruby
javascript_url "common" # => http://www.example.com/assets/common.js
@@ -530,7 +530,7 @@ stylesheet_path "application" # => /assets/application.css
#### stylesheet_url
-Computes the url to a stylesheet asset in the `app/assets/stylesheets` directory. This will call `stylesheet_path` internally and merge with your current host or your asset host.
+Computes the URL to a stylesheet asset in the `app/assets/stylesheets` directory. This will call `stylesheet_path` internally and merge with your current host or your asset host.
```ruby
stylesheet_url "application" # => http://www.example.com/assets/application.css
@@ -599,7 +599,7 @@ This would add something like "Process data files (0.34523)" to the log, which y
#### cache
-A method for caching fragments of a view rather than an entire action or page. This technique is useful for caching pieces like menus, lists of news topics, static HTML fragments, and so on. This method takes a block that contains the content you wish to cache. See `ActionController::Caching::Fragments` for more information.
+A method for caching fragments of a view rather than an entire action or page. This technique is useful for caching pieces like menus, lists of news topics, static HTML fragments, and so on. This method takes a block that contains the content you wish to cache. See `AbstractController::Caching::Fragments` for more information.
```erb
<% cache do %>
@@ -1247,7 +1247,7 @@ file_field_tag 'attachment'
#### form_tag
-Starts a form tag that points the action to a url configured with `url_for_options` just like `ActionController::Base#url_for`.
+Starts a form tag that points the action to a URL configured with `url_for_options` just like `ActionController::Base#url_for`.
```html+erb
<%= form_tag '/articles' do %>
@@ -1419,7 +1419,7 @@ number_to_percentage(100, precision: 0) # => 100%
#### number_to_phone
-Formats a number into a US phone number.
+Formats a number into a phone number (US by default).
```ruby
number_to_phone(1235551234) # => 123-555-1234
@@ -1524,7 +1524,7 @@ Localized Views
Action View has the ability to render different templates depending on the current locale.
-For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available.
+For example, suppose you have an `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available.
You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages.
diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md
index c05e20aceb..e834aeadb1 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -13,7 +13,7 @@ After reading this guide, you will know:
* How an Active Record model behaves.
* How Callbacks and validations work.
* How serializers work.
-* The Rails internationalization (i18n) framework.
+* How Active Model integrates with the Rails internationalization (i18n) framework.
--------------------------------------------------------------------------------
@@ -319,7 +319,7 @@ person.serializable_hash # => {"name"=>"Bob"}
#### ActiveModel::Serializers
-Rails provides a `ActiveModel::Serializers::JSON` serializer.
+Rails provides an `ActiveModel::Serializers::JSON` serializer.
This module automatically include the `ActiveModel::Serialization`.
##### ActiveModel::Serializers::JSON
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index fba89f9d13..d9e9466a33 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -361,7 +361,7 @@ class CreatePublications < ActiveRecord::Migration[5.0]
t.string :publisher_type
t.boolean :single_issue
- t.timestamps null: false
+ t.timestamps
end
add_index :publications, :publication_type_id
end
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
index bd7dbd0f11..f914122242 100644
--- a/guides/source/active_record_migrations.md
+++ b/guides/source/active_record_migrations.md
@@ -41,7 +41,7 @@ class CreateProducts < ActiveRecord::Migration[5.0]
t.string :name
t.text :description
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -247,6 +247,7 @@ end
```
This migration will create a `user_id` column and appropriate index.
+For more `add_reference` options, visit the [API documentation](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_reference).
There is also a generator which will produce join tables if `JoinTable` is part of the name:
@@ -287,7 +288,7 @@ class CreateProducts < ActiveRecord::Migration[5.0]
t.string :name
t.text :description
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -353,7 +354,14 @@ end
```
will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
-(when using MySQL, the default is `ENGINE=InnoDB`).
+(when using MySQL or MariaDB, the default is `ENGINE=InnoDB`).
+
+Also you can pass the `:comment` option with any description for the table
+that will be stored in database itself and can be viewed with database administration
+tools, such as MySQL Workbench or PgAdmin III. It's highly recommended to specify
+comments in migrations for applications with large databases as it helps people
+to understand data model and generate documentation.
+Currently only the MySQL and PostgreSQL adapters support comments.
### Creating a Join Table
@@ -454,6 +462,7 @@ number of digits after the decimal point.
are using a dynamic value (such as a date), the default will only be calculated
the first time (i.e. on the date the migration is applied).
* `index` Adds an index for the column.
+* `comment` Adds a comment for the column.
Some adapters may support additional options; see the adapter specific API docs
for further information.
@@ -847,7 +856,7 @@ class CreateProducts < ActiveRecord::Migration[5.0]
create_table :products do |t|
t.string :name
t.text :description
- t.timestamps null: false
+ t.timestamps
end
end
@@ -970,7 +979,7 @@ this, then you should set the schema format to `:sql`.
Instead of using Active Record's schema dumper, the database's structure will
be dumped using a tool specific to the database (via the `db:structure:dump`
rails task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump`
-utility is used. For MySQL, this file will contain the output of
+utility is used. For MySQL and MariaDB, this file will contain the output of
`SHOW CREATE TABLE` for the various tables.
Loading these schemas is simply a question of executing the SQL statements they
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index 1235c04c50..e9f6275e55 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -11,7 +11,7 @@ After reading this guide, you will know:
* How to specify the order, retrieved attributes, grouping, and other properties of the found records.
* How to use eager loading to reduce the number of database queries needed for data retrieval.
* How to use dynamic finder methods.
-* How to use method chaining to use multiple ActiveRecord methods together.
+* How to use method chaining to use multiple Active Record methods together.
* How to check for the existence of particular records.
* How to perform various calculations on Active Record models.
* How to run EXPLAIN on relations.
@@ -50,7 +50,7 @@ class Role < ApplicationRecord
end
```
-Active Record will perform queries on the database for you and is compatible with most database systems (MySQL, PostgreSQL and SQLite to name a few). Regardless of which database system you're using, the Active Record method format will always be the same.
+Active Record will perform queries on the database for you and is compatible with most database systems, including MySQL, MariaDB, PostgreSQL and SQLite. Regardless of which database system you're using, the Active Record method format will always be the same.
Retrieving Objects from the Database
------------------------------------
@@ -153,9 +153,9 @@ You can pass in a numerical argument to the `take` method to return up to that n
```ruby
client = Client.take(2)
# => [
- #<Client id: 1, first_name: "Lifo">,
- #<Client id: 220, first_name: "Sara">
-]
+# #<Client id: 1, first_name: "Lifo">,
+# #<Client id: 220, first_name: "Sara">
+# ]
```
The SQL equivalent of the above is:
@@ -192,10 +192,10 @@ You can pass in a numerical argument to the `first` method to return up to that
```ruby
client = Client.first(3)
# => [
- #<Client id: 1, first_name: "Lifo">,
- #<Client id: 2, first_name: "Fifo">,
- #<Client id: 3, first_name: "Filo">
-]
+# #<Client id: 1, first_name: "Lifo">,
+# #<Client id: 2, first_name: "Fifo">,
+# #<Client id: 3, first_name: "Filo">
+# ]
```
The SQL equivalent of the above is:
@@ -243,10 +243,10 @@ You can pass in a numerical argument to the `last` method to return up to that n
```ruby
client = Client.last(3)
# => [
- #<Client id: 219, first_name: "James">,
- #<Client id: 220, first_name: "Sara">,
- #<Client id: 221, first_name: "Russel">
-]
+# #<Client id: 219, first_name: "James">,
+# #<Client id: 220, first_name: "Sara">,
+# #<Client id: 221, first_name: "Russel">
+# ]
```
The SQL equivalent of the above is:
@@ -1613,7 +1613,7 @@ now want the client named 'Nick':
```ruby
nick = Client.find_or_initialize_by(first_name: 'Nick')
-# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+# => #<Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
nick.persisted?
# => false
@@ -1645,10 +1645,10 @@ Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
ORDER BY clients.created_at desc")
# => [
- #<Client id: 1, first_name: "Lucas" >,
- #<Client id: 2, first_name: "Jan" >,
- # ...
-]
+# #<Client id: 1, first_name: "Lucas" >,
+# #<Client id: 2, first_name: "Jan" >,
+# ...
+# ]
```
`find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects.
@@ -1660,9 +1660,9 @@ Client.find_by_sql("SELECT * FROM clients
```ruby
Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")
# => [
- {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
- {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
-]
+# {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
+# {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
+# ]
```
### `pluck`
@@ -1915,7 +1915,7 @@ EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`
2 rows in set (0.00 sec)
```
-under MySQL.
+under MySQL and MariaDB.
Active Record performs a pretty printing that emulates that of the
corresponding database shell. So, the same query running with the
@@ -1975,7 +1975,7 @@ EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN
1 row in set (0.00 sec)
```
-under MySQL.
+under MySQL and MariaDB.
### Interpreting EXPLAIN
@@ -1986,4 +1986,6 @@ following pointers may be helpful:
* MySQL: [EXPLAIN Output Format](http://dev.mysql.com/doc/refman/5.7/en/explain-output.html)
+* MariaDB: [EXPLAIN](https://mariadb.com/kb/en/mariadb/explain/)
+
* PostgreSQL: [Using EXPLAIN](http://www.postgresql.org/docs/current/static/using-explain.html)
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index dd7adf09a2..1cf4abce10 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -149,7 +149,7 @@ false` as an argument. This technique should be used with caution.
### `valid?` and `invalid?`
-Before saving an ActiveRecord object, Rails runs your validations.
+Before saving an Active Record object, Rails runs your validations.
If these validations produce any errors, Rails does not save the object.
You can also run these validations on your own. `valid?` triggers your validations
@@ -505,6 +505,8 @@ constraints to acceptable values:
* `:less_than_or_equal_to` - Specifies the value must be less than or equal to
the supplied value. The default error message for this option is _"must be
less than or equal to %{count}"_.
+* `:other_than` - Specifies the value must be other than the supplied value.
+ The default error message for this option is _"must be other than %{count}"_.
* `:odd` - Specifies the value must be an odd number if set to true. The
default error message for this option is _"must be odd"_.
* `:even` - Specifies the value must be an even number if set to true. The
@@ -783,7 +785,7 @@ A `String` `:message` value can optionally contain any/all of `%{value}`,
`%{attribute}`, and `%{model}` which will be dynamically replaced when
validation fails.
-A `Proc` `:message` value is given two arguments: a message key for i18n, and
+A `Proc` `:message` value is given two arguments: the object being validated, and
a hash with `:model`, `:attribute`, and `:value` key-value pairs.
```ruby
@@ -799,10 +801,10 @@ class Person < ApplicationRecord
# Proc
validates :username,
uniqueness: {
- # key = "activerecord.errors.models.person.attributes.username.taken"
+ # object = person object being validated
# data = { model: "Person", attribute: "Username", value: <username> }
- message: ->(key, data) do
- "#{data[:value]} taken! Try again #{Time.zone.tomorrow}"
+ message: ->(object, data) do
+ "Hey #{object.name}!, #{data[:value]} is taken already! Try again #{Time.zone.tomorrow}"
end
}
end
@@ -830,6 +832,25 @@ class Person < ApplicationRecord
end
```
+You can also use `on:` to define custom context.
+Custom contexts need to be triggered explicitly
+by passing name of the context to `valid?`, `invalid?` or `save`.
+
+```ruby
+class Person < ApplicationRecord
+ validates :email, uniqueness: true, on: :account_setup
+ validates :age, numericality: true, on: :account_setup
+end
+
+person = Person.new
+```
+
+`person.valid?(:account_setup)` executes both the validations
+without saving the model. And `person.save(context: :account_setup)`
+validates `person` in `account_setup` context before saving.
+On explicit triggers, model is validated by
+validations of only that context and validations without context.
+
Strict Validations
------------------
@@ -1196,9 +1217,9 @@ person.errors[:name]
person.errors.clear
person.errors.empty? # => true
-p.save # => false
+person.save # => false
-p.errors[:name]
+person.errors[:name]
# => ["can't be blank", "is too short (minimum is 3 characters)"]
```
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 10122629b2..d7f9201b26 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -632,8 +632,6 @@ module ActiveSupport
mattr_accessor :load_once_paths
mattr_accessor :autoloaded_constants
mattr_accessor :explicitly_unloadable_constants
- mattr_accessor :logger
- mattr_accessor :log_activity
mattr_accessor :constant_watch_stack
mattr_accessor :constant_watch_stack_mutex
end
@@ -709,29 +707,6 @@ M.parents # => [X::Y, X, Object]
NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
-### Constants
-
-The method `local_constants` returns the names of the constants that have been
-defined in the receiver module:
-
-```ruby
-module X
- X1 = 1
- X2 = 2
- module Y
- Y1 = :y1
- X1 = :overrides_X1_above
- end
-end
-
-X.local_constants # => [:X1, :X2, :Y]
-X::Y.local_constants # => [:Y1, :X1]
-```
-
-The names are returned as symbols.
-
-NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
-
#### Qualified Constant Names
The standard methods `const_defined?`, `const_get`, and `const_set` accept
@@ -3078,7 +3053,7 @@ INFO: The following calculation methods have edge cases in October 1582, since d
#### `Date.current`
-Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Date.current`.
+Active Support defines `Date.current` to be today in the current time zone. That's like `Date.today`, except that it honors the user time zone, if defined. It also defines `Date.yesterday` and `Date.tomorrow`, and the instance predicates `past?`, `today?`, `future?`, `on_weekday?` and `on_weekend?`, all of them relative to `Date.current`.
When making Date comparisons using methods which honor the user time zone, make sure to use `Date.current` and not `Date.today`. There are cases where the user time zone might be in the future compared to the system time zone, which `Date.today` uses by default. This means `Date.today` may equal `Date.yesterday`.
@@ -3467,6 +3442,8 @@ years_ago
years_since
prev_year (last_year)
next_year
+on_weekday?
+on_weekend?
```
The following methods are reimplemented so you do **not** need to load `active_support/core_ext/date/calculations.rb` for these ones:
@@ -3653,6 +3630,8 @@ years_ago
years_since
prev_year (last_year)
next_year
+on_weekday?
+on_weekend?
```
They are analogous. Please refer to their documentation above and take into account the following differences:
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index 0fd0112c9f..03af3cf819 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -112,6 +112,7 @@ Action Controller
| `:controller` | The controller name |
| `:action` | The action |
| `:params` | Hash of request parameters without any filtered parameter |
+| `:headers` | Request headers |
| `:format` | html/js/json/xml etc |
| `:method` | HTTP request verb |
| `:path` | Request path |
@@ -121,6 +122,7 @@ Action Controller
controller: "PostsController",
action: "new",
params: { "action" => "new", "controller" => "posts" },
+ headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
format: :html,
method: "GET",
path: "/posts/new"
@@ -134,6 +136,7 @@ Action Controller
| `:controller` | The controller name |
| `:action` | The action |
| `:params` | Hash of request parameters without any filtered parameter |
+| `:headers` | Request headers |
| `:format` | html/js/json/xml etc |
| `:method` | HTTP request verb |
| `:path` | Request path |
@@ -146,6 +149,7 @@ Action Controller
controller: "PostsController",
action: "index",
params: {"action" => "index", "controller" => "posts"},
+ headers: #<ActionDispatch::Http::Headers:0x0055a67a519b88>,
format: :html,
method: "GET",
path: "/posts",
diff --git a/guides/source/api_app.md b/guides/source/api_app.md
index 563214896a..8dba914923 100644
--- a/guides/source/api_app.md
+++ b/guides/source/api_app.md
@@ -13,8 +13,8 @@ In this guide you will learn:
--------------------------------------------------------------------------------
-What is an API app?
--------------------
+What is an API Application?
+---------------------------
Traditionally, when people said that they used Rails as an "API", they meant
providing a programmatically accessible API alongside their web application.
@@ -28,15 +28,14 @@ applications.
For example, Twitter uses its [public API](https://dev.twitter.com) in its web
application, which is built as a static site that consumes JSON resources.
-Instead of using Rails to generate dynamic HTML that will communicate with the
-server through forms and links, many developers are treating their web application
-as just another client, delivered as static HTML, CSS and JavaScript consuming
-a simple JSON API.
+Instead of using Rails to generate HTML that communicates with the server
+through forms and links, many developers are treating their web application as
+just an API client delivered as HTML with JavaScript that consumes a JSON API.
This guide covers building a Rails application that serves JSON resources to an
-API client **or** a client-side framework.
+API client, including client-side frameworks.
-Why use Rails for JSON APIs?
+Why Use Rails for JSON APIs?
----------------------------
The first question a lot of people have when thinking about building a JSON API
@@ -75,7 +74,7 @@ Handled at the middleware layer:
URL-encoded String? No problem. Rails will decode the JSON for you and make
it available in `params`. Want to use nested URL-encoded parameters? That
works too.
-- Conditional GETs: Rails handles conditional `GET`, (`ETag` and `Last-Modified`),
+- Conditional GETs: Rails handles conditional `GET` (`ETag` and `Last-Modified`)
processing request headers and returning the correct response headers and status
code. All you need to do is use the
[`stale?`](http://api.rubyonrails.org/classes/ActionController/ConditionalGet.html#method-i-stale-3F)
@@ -104,21 +103,21 @@ Handled at the Action Pack layer:
add the response headers, but why?
- Caching: Rails provides page, action and fragment caching. Fragment caching
is especially helpful when building up a nested JSON object.
-- Basic, Digest and Token Authentication: Rails comes with out-of-the-box support
+- Basic, Digest, and Token Authentication: Rails comes with out-of-the-box support
for three kinds of HTTP authentication.
-- Instrumentation: Rails has an instrumentation API that will trigger registered
+- Instrumentation: Rails has an instrumentation API that triggers registered
handlers for a variety of events, such as action processing, sending a file or
data, redirection, and database queries. The payload of each event comes with
relevant information (for the action processing event, the payload includes
the controller, action, parameters, request format, request method and the
request's full path).
-- Generators: This may be passé for advanced Rails users, but it can be nice to
- generate a resource and get your model, controller, test stubs, and routes
- created for you in a single command.
+- Generators: It is often handy to generate a resource and get your model,
+ controller, test stubs, and routes created for you in a single command for
+ further tweaking. Same for migrations and others.
- Plugins: Many third-party libraries come with support for Rails that reduce
or eliminate the cost of setting up and gluing together the library and the
web framework. This includes things like overriding default generators, adding
- rake tasks, and honoring Rails choices (like the logger and cache back-end).
+ Rake tasks, and honoring Rails choices (like the logger and cache back-end).
Of course, the Rails boot process also glues together all registered components.
For example, the Rails boot process is what uses your `config/database.yml` file
@@ -167,14 +166,23 @@ class definition:
config.api_only = true
```
-Optionally, in `config/environments/development.rb` add the following line
-to render error responses using the API format (JSON by default) when it
-is a local request:
+In `config/environments/development.rb`, set `config.debug_exception_response_format`
+to configure the format used in responses when errors occur in development mode.
+
+To render an HTML page with debugging information, use the value `:default`.
+
+```ruby
+config.debug_exception_response_format = :default
+```
+
+To render debugging information preserving the response format, use the value `:api`.
```ruby
config.debug_exception_response_format = :api
```
+By default, `config.debug_exception_response_format` is set to `:api`.
+
Finally, inside `app/controllers/application_controller.rb`, instead of:
```ruby
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index 4efffc6605..cc3da47db9 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -25,13 +25,9 @@ It allows assets in your application to be automatically combined with assets
from other gems. For example, jquery-rails includes a copy of jquery.js
and enables AJAX features in Rails.
-The asset pipeline is technically no longer a core feature from Rails 4 onwards -- it has
-been extracted out of the framework into the
-[sprockets-rails](https://github.com/rails/sprockets-rails) gem.
-
-The asset pipeline is enabled by default.
-
-You can disable the asset pipeline while creating a new application by
+The asset pipeline is implemented by the
+[sprockets-rails](https://github.com/rails/sprockets-rails) gem,
+and is enabled by default. You can disable it while creating a new application by
passing the `--skip-sprockets` option.
```bash
@@ -335,8 +331,8 @@ include the 'data-turbolinks-track' option which causes turbolinks to check if
an asset has been updated and if so loads it into the page:
```erb
-<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
-<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => "reload" %>
+<%= javascript_include_tag "application", "data-turbolinks-track" => "reload" %>
```
In regular views you can access images in the `public/assets/images` directory
@@ -670,7 +666,7 @@ anymore, delete these options from the `javascript_include_tag` and
`stylesheet_link_tag`.
The fingerprinting behavior is controlled by the `config.assets.digest`
-initialization option (which defaults to `true` for production and development).
+initialization option (which defaults to `true`).
NOTE: Under normal circumstances the default `config.assets.digest` option
should not be changed. If there are no digests in the filenames, and far-future
@@ -789,7 +785,6 @@ location ~ ^/assets/ {
add_header Cache-Control public;
add_header ETag "";
- break;
}
```
@@ -901,7 +896,7 @@ your CDN server, you need to tell browsers to use your CDN to grab assets
instead of your Rails server directly. You can do this by configuring Rails to
set your CDN as the asset host instead of using a relative path. To set your
asset host in Rails, you need to set `config.action_controller.asset_host` in
-`config/production.rb`:
+`config/environments/production.rb`:
```ruby
config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com'
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index 09ab64837a..4977d4f30e 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -105,13 +105,13 @@ class CreateBooks < ActiveRecord::Migration[5.0]
def change
create_table :authors do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :books do |t|
t.belongs_to :author, index: true
t.datetime :published_at
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -136,13 +136,13 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier, index: true
t.string :account_number
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -180,13 +180,13 @@ class CreateAuthors < ActiveRecord::Migration[5.0]
def change
create_table :authors do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :books do |t|
t.belongs_to :author, index: true
t.datetime :published_at
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -222,19 +222,19 @@ class CreateAppointments < ActiveRecord::Migration[5.0]
def change
create_table :physicians do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :patients do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :appointments do |t|
t.belongs_to :physician, index: true
t.belongs_to :patient, index: true
t.datetime :appointment_date
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -308,19 +308,19 @@ class CreateAccountHistories < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier, index: true
t.string :account_number
- t.timestamps null: false
+ t.timestamps
end
create_table :account_histories do |t|
t.belongs_to :account, index: true
t.integer :credit_rating
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -349,12 +349,12 @@ class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
def change
create_table :assemblies do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :parts do |t|
t.string :part_number
- t.timestamps null: false
+ t.timestamps
end
create_table :assemblies_parts, id: false do |t|
@@ -388,13 +388,13 @@ class CreateSuppliers < ActiveRecord::Migration[5.0]
def change
create_table :suppliers do |t|
t.string :name
- t.timestamps null: false
+ t.timestamps
end
create_table :accounts do |t|
t.integer :supplier_id
t.string :account_number
- t.timestamps null: false
+ t.timestamps
end
add_index :accounts, :supplier_id
@@ -472,7 +472,7 @@ class CreatePictures < ActiveRecord::Migration[5.0]
t.string :name
t.integer :imageable_id
t.string :imageable_type
- t.timestamps null: false
+ t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
@@ -488,7 +488,7 @@ class CreatePictures < ActiveRecord::Migration[5.0]
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true, index: true
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -518,7 +518,7 @@ class CreateEmployees < ActiveRecord::Migration[5.0]
def change
create_table :employees do |t|
t.references :manager, index: true
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -545,12 +545,12 @@ author.books.size # uses the cached copy of books
author.books.empty? # uses the cached copy of books
```
-But what if you want to reload the cache, because data might have been changed by some other part of the application? Just pass `true` to the association call:
+But what if you want to reload the cache, because data might have been changed by some other part of the application? Just call `reload` on the association:
```ruby
author.books # retrieves books from the database
author.books.size # uses the cached copy of books
-author.books(true).empty? # discards the cached copy of books
+author.books.reload.empty? # discards the cached copy of books
# and goes back to the database
```
@@ -734,7 +734,7 @@ end
With these changes, Active Record will only load one copy of the author object, preventing inconsistencies and making your application more efficient:
```ruby
-a = author.first
+a = Author.first
b = a.books.first
a.first_name == b.author.first_name # => true
a.first_name = 'Manny'
@@ -932,15 +932,13 @@ side of the association.
Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`.
##### `:dependent`
-If you set the `:dependent` option to:
-
-* `:destroy`, when the object is destroyed, `destroy` will be called on its
-associated objects.
-* `:delete_all`, when the object is destroyed, all its associated objects will be
-deleted directly from the database without calling their `destroy` method.
-* `:nullify`, causes the foreign key to be set to `NULL`. Callbacks are not executed.
-* `:restrict_with_exception`, causes an exception to be raised if there is an associated record
-* `:restrict_with_error`, causes an error to be added to the owner if there is an associated object
+Controls what happens to associated objects when their owner is destroyed:
+
+* `:destroy` causes the associated objects to also be destroyed.
+* `:delete_all` causes the associated objects to be deleted directly from the database (callbacks are not executed).
+* `:nullify` causes the foreign keys to be set to `NULL` (callbacks are not executed).
+* `:restrict_with_exception` causes an exception to be raised if there are associated records.
+* `:restrict_with_error` causes an error to be added to the owner if there are associated objects.
WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database.
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index 3a1a1ccfe6..745f09f523 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -119,25 +119,16 @@ If you want to cache a fragment under certain conditions, you can use
The `render` helper can also cache individual templates rendered for a collection.
It can even one up the previous example with `each` by reading all cache
-templates at once instead of one by one. This is done automatically if the template
-rendered by the collection includes a `cache` call. Take a collection that renders
-a `products/_product.html.erb` partial for each element:
-
-```ruby
-render products
-```
-
-If `products/_product.html.erb` starts with a `cache` call like so:
+templates at once instead of one by one. This is done by passing `cached: true` when rendering the collection:
```html+erb
-<% cache product do %>
- <%= product.name %>
-<% end %>
+<%= render partial: 'products/product', collection: @products, cached: true %>
```
-All the cached templates from previous renders will be fetched at once with much
-greater speed. There's more info on how to make your templates [eligible for
-collection caching](http://api.rubyonrails.org/classes/ActionView/Template/Handlers/ERB.html#method-i-resource_cache_call_pattern).
+All cached templates from previous renders will be fetched at once with much
+greater speed. Additionally, the templates that haven't yet been cached will be
+written to cache and multi fetched on the next render.
+
### Russian Doll Caching
@@ -521,6 +512,40 @@ class ProductsController < ApplicationController
end
```
+### Strong v/s Weak ETags
+
+Rails generates weak ETags by default. Weak ETags allow semantically equivalent
+responses to have the same ETags, even if their bodies do not match exactly.
+This is useful when we don't want the page to be regenerated for minor changes in
+response body.
+
+Weak ETags have a leading `W/` to differentiate them from strong ETags.
+
+```
+ W/"618bbc92e2d35ea1945008b42799b0e7" → Weak ETag
+ "618bbc92e2d35ea1945008b42799b0e7" → Strong ETag
+```
+
+Unlike weak ETag, strong ETag implies that response should be exactly the same
+and byte by byte identical. Useful when doing Range requests within a
+large video or PDF file. Some CDNs support only strong ETags, like Akamai.
+If you absolutely need to generate a strong ETag, it can be done as follows.
+
+```ruby
+ class ProductsController < ApplicationController
+ def show
+ @product = Product.find(params[:id])
+ fresh_when last_modified: @product.published_at.utc, strong_etag: @product
+ end
+ end
+```
+
+You can also set the strong ETag directly on the response.
+
+```ruby
+ response.strong_etag = response.body # => "618bbc92e2d35ea1945008b42799b0e7"
+```
+
References
----------
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index e865a02cbd..0905c4bd16 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -65,11 +65,12 @@ $ bin/rails server
=> Booting Puma
=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
-=> Ctrl-C to shutdown server
-Puma 2.15.3 starting...
-* Min threads: 0, max threads: 16
+Puma starting in single mode...
+* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas
+* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
+Use Ctrl-C to stop
```
With just three commands we whipped up a Rails server listening on port 3000. Go to your browser and open [http://localhost:3000](http://localhost:3000), you will see a basic Rails app running.
@@ -325,7 +326,7 @@ With the `helper` method it is possible to access Rails and your application's h
### `rails dbconsole`
-`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL and SQLite3.
+`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL (including MariaDB), PostgreSQL and SQLite3.
INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`.
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index a5fb396f15..582b14e25f 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -98,15 +98,17 @@ application. Accepts a valid week day symbol (e.g. `:monday`).
* `config.exceptions_app` sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to `ActionDispatch::PublicExceptions.new(Rails.public_path)`.
+* `config.debug_exception_response_format` sets the format used in responses when errors occur in development mode.
+
* `config.file_watcher` is the class used to detect file updates in the file system when `config.reload_classes_only_on_change` is true. Rails ships with `ActiveSupport::FileUpdateChecker`, the default, and `ActiveSupport::EventedFileUpdateChecker` (this one depends on the [listen](https://github.com/guard/listen) gem). Custom classes must conform to the `ActiveSupport::FileUpdateChecker` API.
* `config.filter_parameters` used for filtering out the parameters that
you don't want shown in the logs, such as passwords or credit card
-numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`.
+numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`. Parameters filter works by partial matching regular expression.
-* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details.
+* `config.force_ssl` forces all requests to be served over HTTPS by using the `ActionDispatch::SSL` middleware, and sets `config.action_mailer.default_url_options` to be `{ protocol: 'https' }`. This can be configured by setting `config.ssl_options` - see the [ActionDispatch::SSL documentation](http://edgeapi.rubyonrails.org/classes/ActionDispatch/SSL.html) for details.
-* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`.
+* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`. If you are setting a value for `config.logger` you must manually pass the value of your formatter to your logger before it is wrapped in an `ActiveSupport::TaggedLogging` instance, Rails will not do it for you.
* `config.log_level` defines the verbosity of the Rails logger. This option
defaults to `:debug` for all environments. The available log levels are: `:debug`,
@@ -114,7 +116,21 @@ defaults to `:debug` for all environments. The available log levels are: `:debug
* `config.log_tags` accepts a list of: methods that the `request` object responds to, a `Proc` that accepts the `request` object, or something that responds to `to_s`. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications.
-* `config.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::Logger`.
+* `config.logger` is the logger that will be used for `Rails.logger` and any related Rails logging such as `ActiveRecord::Base.logger`. It defaults to an instance of `ActiveSupport::TaggedLogging` that wraps an instance of `ActiveSupport::Logger` which outputs a log to the `log/` directory. You can supply a custom logger, to get full compatability you must follow these guidelines:
+ * To support a formatter you must manually assign a formatter from the `config.log_formatter` value to the logger.
+ * To support tagged loggs the log instance must be wrapped with `ActiveSupport::TaggedLogging`.
+ * To support silencing the logger must include `LoggerSilence` and `ActiveSupport::LoggerThreadSafeLevel` modules. The `ActiveSupport::Logger` class already includes these modules.
+
+ ```ruby
+ class MyLogger < ::Logger
+ include ActiveSupport::LoggerThreadSafeLevel
+ include LoggerSilence
+ end
+
+ mylogger = MyLogger.new(STDOUT)
+ mylogger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(mylogger)
+ ```
* `config.middleware` allows you to configure the application's middleware. This is covered in depth in the [Configuring Middleware](#configuring-middleware) section below.
@@ -155,7 +171,7 @@ pipeline is enabled. It is set to true by default.
* `config.assets.manifest` defines the full path to be used for the asset precompiler's manifest file. Defaults to a file named `manifest-<random>.json` in the `config.assets.prefix` directory within the public folder.
-* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb` and `development.rb`.
+* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default.
* `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`.
@@ -189,6 +205,7 @@ The full set of methods that can be used in this block are as follows:
* `scaffold_controller` different from `resource_controller`, defines which generator to use for generating a _scaffolded_ controller when using `rails generate scaffold`. Defaults to `:scaffold_controller`.
* `stylesheets` turns on the hook for stylesheets in generators. Used in Rails for when the `scaffold` generator is run, but this hook can be used in other generates as well. Defaults to `true`.
* `stylesheet_engine` configures the stylesheet engine (for eg. sass) to be used when generating assets. Defaults to `:css`.
+* `scaffold_stylesheet` creates `scaffold.css` when generating a scaffolded resource. Defaults to `true`.
* `test_framework` defines which test framework to use. Defaults to `false` and will use Minitest by default.
* `template_engine` defines which template engine to use, such as ERB or Haml. Defaults to `:erb`.
@@ -279,6 +296,8 @@ All these configuration options are delegated to the `I18n` library.
* `config.active_record.schema_format` controls the format for dumping the database schema to a file. The options are `:ruby` (the default) for a database-independent version that depends on migrations, or `:sql` for a set of (potentially database-dependent) SQL statements.
+* `config.active_record.error_on_ignored_order_or_limit` specifies if an error should be raised if the order or limit of a query is ignored during a batch query. The options are true (raise error) or false (warn). Default is false.
+
* `config.active_record.timestamped_migrations` controls whether migrations are numbered with serial integers or with timestamps. The default is true, to use timestamps, which are preferred if there are multiple developers working on the same application.
* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is true by default.
@@ -317,7 +336,7 @@ All these configuration options are delegated to the `I18n` library.
The MySQL adapter adds one additional configuration option:
-* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns in a MySQL database to be booleans and is true by default.
+* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. True by default.
The schema dumper adds one additional configuration option:
@@ -333,7 +352,7 @@ The schema dumper adds one additional configuration option:
* `config.action_controller.default_static_extension` configures the extension used for cached pages. Defaults to `.html`.
-* `config.action_controller.include_all_helpers` configures whether all view helpers are available everywhere or are scoped to the corresponding controller. If set to `false`, `UsersHelper` methods are only available for views rendered as part of `UsersController`. If `true`, `UsersHelper` methods are available everywhere. The default is `true`.
+* `config.action_controller.include_all_helpers` configures whether all view helpers are available everywhere or are scoped to the corresponding controller. If set to `false`, `UsersHelper` methods are only available for views rendered as part of `UsersController`. If `true`, `UsersHelper` methods are available everywhere. The default configuration behavior (when this option is not explicitly set to `true` or `false`) is that all view helpers are available to each controller.
* `config.action_controller.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action Controller. Set to `nil` to disable logging.
@@ -531,6 +550,9 @@ There are a number of settings available on `config.action_mailer`:
* `config.action_mailer.deliver_later_queue_name` specifies the queue name for
mailers. By default this is `mailers`.
+* `config.action_mailer.perform_caching` specifies whether the mailer templates should perform fragment caching or not. By default this is false in all environments.
+
+
### Configuring Active Support
There are a few configuration options available in Active Support:
@@ -545,7 +567,7 @@ There are a few configuration options available in Active Support:
* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`.
-* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. When set to `false`, callback chains are halted only when explicitly done so with `throw(:abort)`. When set to `true`, callback chains are halted when a callback returns false (the previous behavior before Rails 5) and a deprecation warning is given. Defaults to `true` during the deprecation period. New Rails 5 apps generate an initializer file called `callback_terminator.rb` which sets the value to `false`. This file is *not* added when running `rake rails:update`, so returning `false` will still work on older apps ported to Rails 5 and display a deprecation warning to prompt users to update their code.
+* `ActiveSupport.halt_callback_chains_on_return_false` specifies whether Active Record and Active Model callback chains can be halted by returning `false` in a 'before' callback. When set to `false`, callback chains are halted only when explicitly done so with `throw(:abort)`. When set to `true`, callback chains are halted when a callback returns false (the previous behavior before Rails 5) and a deprecation warning is given. Defaults to `true` during the deprecation period. New Rails 5 apps generate an initializer file called `callback_terminator.rb` which sets the value to `false`. This file is *not* added when running `rails app:update`, so returning `false` will still work on older apps ported to Rails 5 and display a deprecation warning to prompt users to update their code.
* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
@@ -561,7 +583,7 @@ There are a few configuration options available in Active Support:
`config.active_job` provides the following configuration options:
-* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:inline` which will perform jobs immediately. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+* `config.active_job.queue_adapter` sets the adapter for the queueing backend. The default adapter is `:async`. For an up-to-date list of built-in adapters see the [ActiveJob::QueueAdapters API documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
```ruby
# Be sure to have the adapter's gem in your Gemfile
@@ -610,6 +632,17 @@ There are a few configuration options available in Active Support:
* `config.active_job.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Active Job. You can retrieve this logger by calling `logger` on either an Active Job class or an Active Job instance. Set to `nil` to disable logging.
+### Configuring Action Cable
+
+* `config.action_cable.url` accepts a string for the URL for where
+ you are hosting your Action Cable server. You would use this option
+if you are running Action Cable servers that are separated from your
+main application.
+* `config.action_cable.mount_path` accepts a string for where to mount Action
+ Cable, as part of the main server process. Defaults to `/cable`.
+You can set this as nil to not mount Action Cable as part of your
+normal Rails server.
+
### Configuring a Database
Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`.
@@ -751,11 +784,11 @@ development:
timeout: 5000
```
-NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it.
+NOTE: Rails uses an SQLite3 database for data storage by default because it is a zero configuration database that just works. Rails also supports MySQL (including MariaDB) and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it.
-#### Configuring a MySQL Database
+#### Configuring a MySQL or MariaDB Database
-If you choose to use MySQL instead of the shipped SQLite3 database, your `config/database.yml` will look a little different. Here's the development section:
+If you choose to use MySQL or MariaDB instead of the shipped SQLite3 database, your `config/database.yml` will look a little different. Here's the development section:
```yaml
development:
@@ -768,7 +801,7 @@ development:
socket: /tmp/mysql.sock
```
-If your development computer's MySQL installation includes a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate.
+If your development database has a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the `development` section as appropriate.
#### Configuring a PostgreSQL Database
@@ -810,9 +843,9 @@ development:
database: db/development.sqlite3
```
-#### Configuring a MySQL Database for JRuby Platform
+#### Configuring a MySQL or MariaDB Database for JRuby Platform
-If you choose to use MySQL and are using JRuby, your `config/database.yml` will look a little different. Here's the development section:
+If you choose to use MySQL or MariaDB and are using JRuby, your `config/database.yml` will look a little different. Here's the development section:
```yaml
development:
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index 0f98d12217..12d0280116 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -367,7 +367,7 @@ Finally,
$ bundle exec rake test
```
-will now run the four of them in turn.
+will now run the three of them in turn.
You can also run any single test separately:
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index 35ad6eb705..877c87e9fa 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -109,18 +109,18 @@ It can also be useful to save information to log files at runtime. Rails maintai
Rails makes use of the `ActiveSupport::Logger` class to write log information. Other loggers, such as `Log4r`, may also be substituted.
-You can specify an alternative logger in `environment.rb` or any other environment file, for example:
+You can specify an alternative logger in `config/application.rb` or any other environment file, for example:
```ruby
-Rails.logger = Logger.new(STDOUT)
-Rails.logger = Log4r::Logger.new("Application Log")
+config.logger = Logger.new(STDOUT)
+config.logger = Log4r::Logger.new("Application Log")
```
Or in the `Initializer` section, add _any_ of the following
```ruby
-config.logger = Logger.new(STDOUT)
-config.logger = Log4r::Logger.new("Application Log")
+Rails.logger = Logger.new(STDOUT)
+Rails.logger = Log4r::Logger.new("Application Log")
```
TIP: By default, each log is created under `Rails.root/log/` and the log file is named after the environment in which the application is running.
@@ -314,11 +314,12 @@ For example:
=> Booting Puma
=> Rails 5.0.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
-=> Ctrl-C to shutdown server
-Puma 2.15.3 starting...
-* Min threads: 0, max threads: 16
+Puma starting in single mode...
+* Version 3.0.2 (ruby 2.3.0-p0), codename: Plethora of Penguin Pinatas
+* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://localhost:3000
+Use Ctrl-C to stop
Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200
diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md
index 7beb8f72a9..cc24e6f666 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -30,7 +30,6 @@ Ruby on Rails uses Git for source code control. The [Git homepage](http://git-sc
* [Try Git course](http://try.github.io/) is an interactive course that will teach you the basics.
* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git.
* [Everyday Git](http://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by.
-* The [PeepCode screencast](https://peepcode.com/products/git) on Git is easier to follow.
* [GitHub](http://help.github.com) offers links to a variety of Git resources.
* [Pro Git](http://git-scm.com/book) is an entire book about Git with a Creative Commons license.
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 2cf613f47f..03943d0f25 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -116,7 +116,7 @@
name: The Rails Initialization Process
work_in_progress: true
url: initialization.html
- description: This guide explains the internals of the Rails initialization process as of Rails 4.
+ description: This guide explains the internals of the Rails initialization process.
-
name: Autoloading and Reloading Constants
url: autoloading_and_reloading_constants.html
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 415def8367..eafac4828c 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -402,8 +402,8 @@ module Blorgh
end
```
-NOTE: The `ApplicationController` class being inherited from here is the
-`Blorgh::ApplicationController`, not an application's `ApplicationController`.
+NOTE: The `ArticlesController` class inherits from
+`Blorgh::ApplicationController`, not the application's `ApplicationController`.
The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced:
@@ -461,7 +461,7 @@ model, a comment controller and then modify the articles scaffold to display
comments and allow people to create new ones.
From the application root, run the model generator. Tell it to generate a
-`Comment` model, with the related table having two columns: a `article_id` integer
+`Comment` model, with the related table having two columns: an `article_id` integer
and `text` text column.
```bash
@@ -799,7 +799,7 @@ before the article is saved. It will also need to have an `attr_accessor` set up
for this field, so that the setter and getter methods are defined for it.
To do all this, you'll need to add the `attr_accessor` for `author_name`, the
-association for the author and the `before_save` call into
+association for the author and the `before_validation` call into
`app/models/blorgh/article.rb`. The `author` association will be hard-coded to the
`User` class for the time being.
@@ -807,7 +807,7 @@ association for the author and the `before_save` call into
attr_accessor :author_name
belongs_to :author, class_name: "User"
-before_save :set_author
+before_validation :set_author
private
def set_author
@@ -1209,7 +1209,7 @@ module Blorgh::Concerns::Models::Article
attr_accessor :author_name
belongs_to :author, class_name: "User"
- before_save :set_author
+ before_validation :set_author
private
def set_author
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index 2a289dd33a..048fe190e8 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -174,7 +174,6 @@ URL fields, email fields, number fields and range fields:
<%= search_field(:user, :name) %>
<%= telephone_field(:user, :phone) %>
<%= date_field(:user, :born_on) %>
-<%= datetime_field(:user, :meeting_time) %>
<%= datetime_local_field(:user, :graduation_day) %>
<%= month_field(:user, :birthday_month) %>
<%= week_field(:user, :birthday_week) %>
@@ -195,7 +194,6 @@ Output:
<input id="user_name" name="user[name]" type="search" />
<input id="user_phone" name="user[phone]" type="tel" />
<input id="user_born_on" name="user[born_on]" type="date" />
-<input id="user_meeting_time" name="user[meeting_time]" type="datetime" />
<input id="user_graduation_day" name="user[graduation_day]" type="datetime-local" />
<input id="user_birthday_month" name="user[birthday_month]" type="month" />
<input id="user_birthday_week" name="user[birthday_week]" type="week" />
@@ -317,7 +315,7 @@ The Article model is directly available to users of the application, so - follow
resources :articles
```
-TIP: Declaring a resource has a number of side-affects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources.
+TIP: Declaring a resource has a number of side effects. See [Rails Routing From the Outside In](routing.html#resource-routing-the-rails-default) for more information on setting up and using resources.
When dealing with RESTful resources, calls to `form_for` can get significantly easier if you rely on **record identification**. In short, you can just pass the model instance and have Rails figure out model name and the rest:
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 8eb3b6190f..ae631ae58d 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -223,8 +223,7 @@ the server.
The "Welcome aboard" page is the _smoke test_ for a new Rails application: it
makes sure that you have your software configured correctly enough to serve a
-page. You can also click on the _About your application's environment_ link to
-see a summary of your application's environment.
+page.
### Say "Hello", Rails
@@ -298,26 +297,30 @@ Open the file `config/routes.rb` in your editor.
Rails.application.routes.draw do
get 'welcome/index'
- # The priority is based upon order of creation:
- # first created -> highest priority.
- # See how all your routes lay out with "bin/rails routes".
- #
- # You can have the root of your site routed with "root"
- # root 'welcome#index'
- #
- # ...
+ # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
+
+ # Serve websocket cable requests in-process
+ # mount ActionCable.server => '/cable'
+end
```
This is your application's _routing file_ which holds entries in a special
[DSL (domain-specific language)](http://en.wikipedia.org/wiki/Domain-specific_language)
that tells Rails how to connect incoming requests to
-controllers and actions. This file contains many sample routes on commented
-lines, and one of them actually shows you how to connect the root of your site
-to a specific controller and action. Find the line beginning with `root` and
-uncomment it. It should look something like the following:
+controllers and actions.
+Edit this file by adding the line of code `root 'welcome#index'`.
+It should look something like the following:
```ruby
-root 'welcome#index'
+Rails.application.routes.draw do
+ get 'welcome/index'
+
+ # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
+
+ # Serve websocket cable requests in-process
+ # mount ActionCable.server => '/cable'
+ root 'welcome#index'
+end
```
`root 'welcome#index'` tells Rails to map requests to the root of the
@@ -348,7 +351,7 @@ operations are referred to as _CRUD_ operations.
Rails provides a `resources` method which can be used to declare a standard REST
resource. You need to add the _article resource_ to the
-`config/routes.rb` as follows:
+`config/routes.rb` so the file will look as follows:
```ruby
Rails.application.routes.draw do
@@ -625,7 +628,7 @@ end
The `render` method here is taking a very simple hash with a key of `:plain` and
value of `params[:article].inspect`. The `params` method is the object which
represents the parameters (or fields) coming in from the form. The `params`
-method returns an `ActiveSupport::HashWithIndifferentAccess` object, which
+method returns an `ActionController::Parameters` object, which
allows you to access the keys of the hash using either strings or symbols. In
this situation, the only parameters that matter are the ones from the form.
@@ -635,7 +638,7 @@ If you re-submit the form one more time you'll now no longer get the missing
template error. Instead, you'll see something that looks like the following:
```ruby
-{"title"=>"First article!", "text"=>"This is my first article."}
+<ActionController::Parameters {"title"=>"First Article!", "text"=>"This is my first article."} permitted: false>
```
This action is now displaying the parameters for the article that are coming in
@@ -686,7 +689,7 @@ class CreateArticles < ActiveRecord::Migration[5.0]
t.string :title
t.text :text
- t.timestamps null: false
+ t.timestamps
end
end
end
@@ -1554,9 +1557,9 @@ class CreateComments < ActiveRecord::Migration[5.0]
create_table :comments do |t|
t.string :commenter
t.text :body
- t.references :article, index: true, foreign_key: true
+ t.references :article, foreign_key: true
- t.timestamps null: false
+ t.timestamps
end
end
end
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index 5bbd4048b9..0edfa072f8 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -25,7 +25,7 @@ After reading this guide, you will know:
* How I18n works in Ruby on Rails
* How to correctly use I18n into a RESTful application in various ways
-* How to use I18n to translate ActiveRecord errors or ActionMailer E-mail subjects
+* How to use I18n to translate Active Record errors or Action Mailer E-mail subjects
* Some other tools to go further with the translation process of your application
--------------------------------------------------------------------------------
@@ -1113,7 +1113,7 @@ Conclusion
At this point you should have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project.
-If you find anything missing or wrong in this guide, please file a ticket on our [issue tracker](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview). If you want to discuss certain portions or have questions, please sign up to our [mailing list](http://groups.google.com/group/rails-i18n).
+If you want to discuss certain portions or have questions, please sign up to the [rails-i18n mailing list](http://groups.google.com/group/rails-i18n).
Contributing to Rails I18n
@@ -1130,10 +1130,8 @@ Resources
---------
* [Google group: rails-i18n](http://groups.google.com/group/rails-i18n) - The project's mailing list.
-* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n/tree/master) - Code repository for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases.
-* [GitHub: i18n](https://github.com/svenfuchs/i18n/tree/master) - Code repository for the i18n gem.
-* [Lighthouse: rails-i18n](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview) - Issue tracker for the rails-i18n project.
-* [Lighthouse: i18n](http://i18n.lighthouseapp.com/projects/14947-ruby-i18n/overview) - Issue tracker for the i18n gem.
+* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n) - Code repository and issue tracker for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases.
+* [GitHub: i18n](https://github.com/svenfuchs/i18n) - Code repository and issue tracker for the i18n gem.
Authors
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index 156f9c92b4..89e5346d86 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -3,8 +3,8 @@
The Rails Initialization Process
================================
-This guide explains the internals of the initialization process in Rails
-as of Rails 4. It is an extremely in-depth guide and recommended for advanced Rails developers.
+This guide explains the internals of the initialization process in Rails.
+It is an extremely in-depth guide and recommended for advanced Rails developers.
After reading this guide, you will know:
@@ -356,8 +356,6 @@ private
def print_boot_information
...
puts "=> Run `rails server -h` for more startup options"
- ...
- puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
end
def create_tmp_directories
diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb
index 1f81ea4694..6db76b528e 100644
--- a/guides/source/layout.html.erb
+++ b/guides/source/layout.html.erb
@@ -24,7 +24,17 @@
<% end %>
<div id="topNav">
<div class="wrapper">
- <strong class="more-info-label">←<a href="http://rubyonrails.org/">Back to rubyonrails.org:</a> </strong>
+ <strong class="more-info-label">More at <a href="http://rubyonrails.org/">rubyonrails.org:</a> </strong>
+ <span class="red-button more-info-button">
+ More Ruby on Rails
+ </span>
+ <ul class="more-info-links s-hidden">
+ <li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
+ <li class="more-info"><a href="http://guides.rubyonrails.org/">Guides</a></li>
+ <li class="more-info"><a href="http://api.rubyonrails.org/">API</a></li>
+ <li class="more-info"><a href="http://stackoverflow.com/questions/tagged/ruby-on-rails">Ask for help</a></li>
+ <li class="more-info"><a href="https://github.com/rails/rails">Contribute on GitHub</a></li>
+ </ul>
</div>
</div>
<div id="header">
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 6946eb81eb..2722789c49 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -149,23 +149,22 @@ render template: "products/show"
#### Rendering an Arbitrary File
-The `render` method can also use a view that's entirely outside of your application (perhaps you're sharing views between two Rails applications):
-
-```ruby
-render "/u/apps/warehouse_app/current/app/views/products/show"
-```
-
-Rails determines that this is a file render because of the leading slash character. To be explicit, you can use the `:file` option (which was required on Rails 2.2 and earlier):
+The `render` method can also use a view that's entirely outside of your application:
```ruby
render file: "/u/apps/warehouse_app/current/app/views/products/show"
```
-The `:file` option takes an absolute file-system path. Of course, you need to have rights to the view that you're using to render the content.
+The `:file` option takes an absolute file-system path. Of course, you need to have rights
+to the view that you're using to render the content.
+
+NOTE: Using the `:file` option in combination with users input can lead to security problems
+since an attacker could use this action to access security sensitive files in your file system.
NOTE: By default, the file is rendered using the current layout.
-TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to render a file, because Windows filenames do not have the same format as Unix filenames.
+TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to
+render a file, because Windows filenames do not have the same format as Unix filenames.
#### Wrapping it up
@@ -238,7 +237,7 @@ TIP: This is useful when you're rendering a small snippet of HTML code.
However, you might want to consider moving it to a template file if the markup
is complex.
-NOTE: When using `html:` option, HTML entities will be escaped if the string is not marked as HTML safe by using `html_safe` method.
+NOTE: When using `html:` option, HTML entities will be escaped if the string is not marked as HTML safe by using `html_safe` method.
#### Rendering JSON
@@ -555,7 +554,7 @@ class Admin::ProductsController < AdminController
end
```
-The lookup order for a `admin/products#index` action will be:
+The lookup order for an `admin/products#index` action will be:
* `app/views/admin/products/`
* `app/views/admin/`
@@ -700,7 +699,7 @@ This would detect that there are no books with the specified ID, populate the `@
### Using `head` To Build Header-Only Responses
-The `head` method can be used to send responses with only headers to the browser. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing a HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header:
+The `head` method can be used to send responses with only headers to the browser. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing an HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header:
```ruby
head :bad_request
diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md
index 5a46baff2d..3b773d84f8 100644
--- a/guides/source/rails_application_templates.md
+++ b/guides/source/rails_application_templates.md
@@ -22,11 +22,11 @@ $ rails new blog -m ~/template.rb
$ rails new blog -m http://example.com/template.rb
```
-You can use the rake task `rails:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL.
+You can use the task `app:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL.
```bash
-$ bin/rails rails:template LOCATION=~/template.rb
-$ bin/rails rails:template LOCATION=http://example.com/template.rb
+$ bin/rails app:template LOCATION=~/template.rb
+$ bin/rails app:template LOCATION=http://example.com/template.rb
```
Template API
@@ -38,7 +38,7 @@ The Rails templates API is easy to understand. Here's an example of a typical Ra
# template.rb
generate(:scaffold, "person name:string")
route "root to: 'people#index'"
-rake("db:migrate")
+rails_command("db:migrate")
after_bundle do
git :init
@@ -175,18 +175,24 @@ Executes an arbitrary command. Just like the backticks. Let's say you want to re
run "rm README.rdoc"
```
-### rake(command, options = {})
+### rails_command(command, options = {})
-Runs the supplied rake tasks in the Rails application. Let's say you want to migrate the database:
+Runs the supplied task in the Rails application. Let's say you want to migrate the database:
```ruby
-rake "db:migrate"
+rails_command "db:migrate"
```
-You can also run rake tasks with a different Rails environment:
+You can also run tasks with a different Rails environment:
```ruby
-rake "db:migrate", env: 'production'
+rails_command "db:migrate", env: 'production'
+```
+
+You can also run tasks as a super-user:
+
+```ruby
+rails_command "log:clear", sudo: true
```
### route(routing_code)
@@ -226,7 +232,7 @@ CODE
These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to Freeze Rails only if the user wants to:
```ruby
-rake("rails:freeze:gems") if yes?("Freeze rails gems?")
+rails_command("rails:freeze:gems") if yes?("Freeze rails gems?")
# no?(question) acts just the opposite.
```
diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md
index 3b61d65df5..b712965b7f 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -93,7 +93,7 @@ NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`,
### Inspecting Middleware Stack
-Rails has a handy rake task for inspecting the middleware stack in use:
+Rails has a handy task for inspecting the middleware stack in use:
```bash
$ bin/rails middleware
diff --git a/guides/source/routing.md b/guides/source/routing.md
index bd3e236a2b..81321c7405 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -812,10 +812,10 @@ In all of these cases, if you don't provide the leading host (`http://www.exampl
Instead of a String like `'articles#index'`, which corresponds to the `index` action in the `ArticlesController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher:
```ruby
-match '/application.js', to: Sprockets, via: :all
+match '/application.js', to: MyRackApp, via: :all
```
-As long as `Sprockets` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate.
+As long as `MyRackApp` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate.
NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application.
diff --git a/guides/source/security.md b/guides/source/security.md
index 98324141cc..16c5291037 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -160,7 +160,7 @@ The most effective countermeasure is to _issue a new session identifier_ and dec
reset_session
```
-If you use the popular RestfulAuthentication plugin for user management, add reset_session to the SessionsController#create action. Note that this removes any value from the session, _you have to transfer them to the new session_.
+If you use the popular [Devise](https://rubygems.org/gems/devise) gem for user management, it will automatically expire sessions on sign in and sign out for you. If you roll your own, remember to expire the session after your sign in action (when the session is created). This will remove values from the session, therefore _you will have to transfer them to the new session_.
Another countermeasure is to _save user-specific properties in the session_, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. _These might change over the course of a session_, so these users will not be able to use your application, or only in a limited way.
@@ -381,7 +381,7 @@ Refer to the Injection section for countermeasures against XSS. It is _recommend
**CSRF** Cross-Site Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
-A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/news/item/Symantec-reports-first-active-attack-on-a-DSL-router-735883.html). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for the user, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen.
+A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/news/item/Symantec-reports-first-active-attack-on-a-DSL-router-735883.html). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for the user, but it also contained an image tag that resulted in an HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen.
Another example changed Google Adsense's e-mail address and password. If the victim was logged into Google Adsense, the administration interface for Google advertisement campaigns, an attacker could change the credentials of the victim.

@@ -453,7 +453,7 @@ However, the attacker may also take over the account by changing the e-mail addr
#### Other
-Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in a HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
+Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in an HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
### CAPTCHAs
@@ -466,7 +466,7 @@ The problem with CAPTCHAs is that they have a negative impact on the user experi
Most bots are really dumb. They crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript.
-Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require a HTTPS request to Google ReCaptcha before computing the response.
+Note that negative CAPTCHAs are only effective against dumb bots and won't suffice to protect critical applications from targeted bots. Still, the negative and positive CAPTCHAs can be combined to increase the performance, e.g., if the "honeypot" field is not empty (bot detected), you won't need to verify the positive CAPTCHA, which would require an HTTPS request to Google ReCaptcha before computing the response.
Here are some ideas how to hide honeypot fields by JavaScript and/or CSS:
@@ -494,6 +494,8 @@ By default, Rails logs all requests being made to the web application. But log f
config.filter_parameters << :password
```
+NOTE: Provided parameters will be filtered out by partial matching regular expression. Rails adds default `:password` in the appropriate initializer (`initializers/filter_parameter_logging.rb`) and cares about typical application parameters `password` and `password_confirmation`.
+
### Good Passwords
INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._
diff --git a/guides/source/testing.md b/guides/source/testing.md
index 1c64b2c0ac..34c831c802 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -287,7 +287,7 @@ specify to make your test failure messages clearer.
| `assert_not_in_delta( expected, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
| `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.|
| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.|
-| `assert_nothing_raised( exception1, exception2, ... ) { block }` | Ensures that the given block doesn't raise one of the given exceptions.|
+| `assert_nothing_raised { block }` | Ensures that the given block doesn't raise any exceptions.|
| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.|
| `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.|
| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class` or is descending from it.|
@@ -495,7 +495,8 @@ users(:david)
users(:david).id
# one can also access methods available on the User class
-email(david.partner.email, david.location_tonight)
+david = users(:david)
+david.call(david.partner)
```
To get multiple fixtures at once, you can pass in a list of fixture names. For example:
@@ -798,7 +799,7 @@ and
can be set directly on the `@request` instance variable:
```ruby
-# setting a HTTP Header
+# setting an HTTP Header
@request.headers["Accept"] = "text/plain, text/html"
get articles_url # simulate the request with custom header
@@ -830,7 +831,7 @@ end
If we run our test now, we should see a failure:
```bash
-$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article
+$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Run options: -n test_should_create_article --seed 32266
# Running:
@@ -868,7 +869,7 @@ end
Now if we run our tests, we should see it pass:
```bash
-$ bin/rails test test/controllers/articles_controller_test.rb test_should_create_article
+$ bin/rails test test/controllers/articles_controller_test.rb -n test_should_create_article
Run options: -n test_should_create_article --seed 18981
# Running:
@@ -1191,9 +1192,9 @@ testing) but instead it will be appended to an array
(`ActionMailer::Base.deliveries`).
NOTE: The `ActionMailer::Base.deliveries` array is only reset automatically in
-`ActionMailer::TestCase` tests. If you want to have a clean slate outside Action
-Mailer tests, you can reset it manually with:
-`ActionMailer::Base.deliveries.clear`
+`ActionMailer::TestCase` and `ActionDispatch::IntegrationTest` tests.
+If you want to have a clean slate outside these test cases, you can reset it
+manually with: `ActionMailer::Base.deliveries.clear`
### Functional Testing
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index cef83e4264..34cfb742a4 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -27,7 +27,7 @@ The process should go as follows:
3. Fix tests and deprecated features
4. Move to the latest patch version of the next minor version
-Repeat this process until you reach your target Rails version. Each time you move versions, you will need to change the Rails version number in the Gemfile (and possibly other gem versions) and run `bundle update`. Then run the Update rake task mentioned below to update configuration files, then run your tests.
+Repeat this process until you reach your target Rails version. Each time you move versions, you will need to change the Rails version number in the Gemfile (and possibly other gem versions) and run `bundle update`. Then run the Update task mentioned below to update configuration files, then run your tests.
You can find a list of all released Rails versions [here](https://rubygems.org/gems/rails/versions).
@@ -42,15 +42,15 @@ Rails generally stays close to the latest released Ruby version when it's releas
TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing.
-### The Rake Task
+### The Task
-Rails provides the `rails:update` rake task. After updating the Rails version
-in the Gemfile, run this rake task.
+Rails provides the `app:update` task (`rails:update` on 4.2 and earlier). After updating the Rails version
+in the Gemfile, run this task.
This will help you with the creation of new files and changes of old files in an
interactive session.
```bash
-$ rake rails:update
+$ rails app:update
identical config/boot.rb
exist config
conflict config/routes.rb
@@ -70,7 +70,8 @@ Upgrading from Rails 4.2 to Rails 5.0
### Ruby 2.2.2+
-ToDo...
+From Ruby on Rails 5.0 onwards, Ruby 2.2.2+ is the only supported version.
+Make sure you are on Ruby 2.2.2 version or greater, before you proceed.
### Active Record models now inherit from ApplicationRecord by default
@@ -183,7 +184,7 @@ the logs. In the next version, these errors will no longer be suppressed.
Instead, the errors will propagate normally just like in other Active
Record callbacks.
-When you define a `after_rollback` or `after_commit` callback, you
+When you define an `after_rollback` or `after_commit` callback, you
will receive a deprecation warning about this upcoming change. When
you are ready, you can opt into the new behavior and remove the
deprecation warning by adding following configuration to your
@@ -417,7 +418,7 @@ secrets, you need to:
3. Remove the `secret_token.rb` initializer.
-4. Use `rails secret` to generate new keys for the `development` and `test` sections.
+4. Use `rake secret` to generate new keys for the `development` and `test` sections.
5. Restart your server.
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index 26ff5da7a3..c58aee96db 100644
--- a/guides/source/working_with_javascript_in_rails.md
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -350,8 +350,8 @@ $("<%= escape_javascript(render @user) %>").appendTo("#users");
Turbolinks
----------
-Rails 4 ships with the [Turbolinks gem](https://github.com/turbolinks/turbolinks).
-This gem uses Ajax to speed up page rendering in most applications.
+Rails ships with the [Turbolinks library](https://github.com/turbolinks/turbolinks),
+which uses Ajax to speed up page rendering in most applications.
### How Turbolinks Works
@@ -364,14 +364,14 @@ will then use PushState to change the URL to the correct one, preserving
refresh semantics and giving you pretty URLs.
The only thing you have to do to enable Turbolinks is have it in your Gemfile,
-and put `//= require turbolinks` in your CoffeeScript manifest, which is usually
+and put `//= require turbolinks` in your JavaScript manifest, which is usually
`app/assets/javascripts/application.js`.
-If you want to disable Turbolinks for certain links, add a `data-no-turbolink`
+If you want to disable Turbolinks for certain links, add a `data-turbolinks="false"`
attribute to the tag:
```html
-<a href="..." data-no-turbolink>No turbolinks here</a>.
+<a href="..." data-turbolinks="false">No turbolinks here</a>.
```
### Page Change Events
@@ -389,7 +389,7 @@ event that this relies on will not be fired. If you have code that looks like
this, you must change your code to do this instead:
```coffeescript
-$(document).on "page:change", ->
+$(document).on "turbolinks:load", ->
alert "page has loaded!"
```
diff --git a/load_paths.rb b/load_paths.rb
deleted file mode 100644
index 0ad8fcfeda..0000000000
--- a/load_paths.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# bust gem prelude
-require 'bundler'
-Bundler.setup
diff --git a/rails.gemspec b/rails.gemspec
index d4e78b6aa1..8f9cc181a0 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -14,7 +14,7 @@ Gem::Specification.new do |s|
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.homepage = 'http://rubyonrails.org'
s.files = ['README.md']
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 2506baac16..32bfdf272b 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,5 +1,44 @@
+* Make `rails restart` command work with Puma by passing the restart command
+ which Puma can use to restart rails server.
+
+ *Prathamesh Sonpatki*
+
+* The application generator writes a new file `config/spring.rb`, which tells
+ Spring to watch additional common files.
+
+ *Xavier Noria*
+
+* The tasks in the rails task namespace is deprecated in favor of app namespace.
+ (e.g. `rails:update` and `rails:template` tasks is renamed to `app:update` and `app:template`.)
+
+ *Ryo Hashimoto*
+
+* Enable HSTS with IncludeSudomains header for new applications.
+
+ *Egor Homakov*, *Prathamesh Sonpatki*
+
+## Rails 5.0.0.beta3 (February 24, 2016) ##
+
+* Alias `rake` with `rails_command` in the Rails Application Templates API
+ following Rails 5 convention of preferring "rails" to "rake" to run tasks.
+
+ *claudiob*
+
+* Generate applications with an option to log to STDOUT in production
+ using the environment variable `RAILS_LOG_TO_STDOUT`.
+
+ *Richard Schneeman*
+
+* Change fail fast of `bin/rails test` interrupts run on error.
+
+ *Yuji Yaginuma*
+
+* The application generator supports `--skip-listen` to opt-out of features
+ that depend on the listen gem. As of this writing they are the evented file
+ system monitor and the async plugin for spring.
+
* The Gemfiles of new applications include spring-watcher-listen on Linux and
- Mac OS X (unless --skip-spring).
+ Mac OS X (unless `--skip-spring`).
*Xavier Noria*
@@ -8,7 +47,9 @@
*Xavier Noria*
-* Add dummy files for apple-touch-icon.png and apple-touch-icon.png. GH#23427
+* Add dummy files for apple-touch-icon.png and apple-touch-icon.png.
+
+ See #23427.
*Alexey Zabelin*
@@ -72,7 +113,7 @@
*Chuck Callebs*
-* Allow use of minitest-rails gem with Rails test runner.
+* Allow use of `minitest-rails` gem with Rails test runner.
Fixes #22455.
@@ -85,13 +126,13 @@
*Yuji Yaginuma*
* Make `static_index` part of the `config.public_file_server` config and
- call it `public_file_server.index_name`.
+ call it `config.public_file_server.index_name`.
*Yuki Nishijima*
-* Deprecate `serve_static_files` in favor of `public_file_server.enabled`.
+* Deprecate `config.serve_static_files` in favor of `config.public_file_server.enabled`.
- Unifies the static asset options under `public_file_server`.
+ Unifies the static asset options under `config.public_file_server`.
To upgrade, replace occurrences of:
@@ -129,8 +170,8 @@
*Yuki Nishijima*
-* Route generator should be idempotent
- running generators several times no longer require you to cleanup routes.rb
+* Route generators are now idempotent.
+ Running generators several times no longer require you to cleanup routes.rb.
*Thiago Pinto*
@@ -138,7 +179,7 @@
*Simon Eskildsen*
-* Allow rake:stats to account for rake tasks in lib/tasks
+* Allow `rake stats` to account for rake tasks in lib/tasks.
*Kevin Deisz*
@@ -148,7 +189,7 @@
*James Kerr*
-* Add fail fast to `bin/rails test`
+* Add fail fast to `bin/rails test`.
Adding `--fail-fast` or `-f` when running tests will interrupt the run on
the first failure:
@@ -180,7 +221,7 @@
*Kasper Timm Hansen*
-* Add inline output to `bin/rails test`
+* Add inline output to `bin/rails test`.
Any failures or errors (and skips if running in verbose mode) are output
during a test run:
@@ -211,7 +252,7 @@
*Kasper Timm Hansen*
* Fix displaying mailer previews on non local requests when config
- `action_mailer.show_previews` is set
+ `config.action_mailer.show_previews` is set.
*Wojciech Wnętrzak*
@@ -242,14 +283,14 @@
*Ersin Akinci*
* Make enabling or disabling caching in development mode possible with
- rake dev:cache.
+ `rake dev:cache`.
- Running rake dev:cache will create or remove tmp/caching-dev.txt. When this
- file exists config.action_controller.perform_caching will be set to true in
+ Running `rake dev:cache` will create or remove tmp/caching-dev.txt. When this
+ file exists `config.action_controller.perform_caching` will be set to true in
config/environments/development.rb.
- Additionally, a server can be started with either --dev-caching or
- --no-dev-caching included to toggle caching on startup.
+ Additionally, a server can be started with either `--dev-caching` or
+ `--no-dev-caching` included to toggle caching on startup.
*Jussi Mertanen*, *Chuck Callebs*
@@ -262,11 +303,11 @@
*Yuji Yaginuma*
-* Adding support for passing a block to the `add_source` action of a custom generator
+* Adding support for passing a block to the `add_source` action of a custom generator.
*Mike Dalton*, *Hirofumi Wakasugi*
-* `assert_file` understands paths with special characters
+* `assert_file` now understands paths with special characters
(eg. `v0.1.4~alpha+nightly`).
*Diego Carrion*
diff --git a/railties/lib/rails/app_loader.rb b/railties/lib/rails/app_loader.rb
index a9fe21824e..af004d85bf 100644
--- a/railties/lib/rails/app_loader.rb
+++ b/railties/lib/rails/app_loader.rb
@@ -16,7 +16,7 @@ like any other source code, rather than stubs that are generated on demand.
Here's how to upgrade:
bundle config --delete bin # Turn off Bundler's stub generator
- rake rails:update:bin # Use the new Rails 4 executables
+ rails app:update:bin # Use the new Rails 5 executables
git add bin # Add bin/ to source control
You may need to remove bin/ from your .gitignore as well.
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index cac31e1eed..ed106c9918 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -1,4 +1,3 @@
-require 'fileutils'
require 'yaml'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/blank'
@@ -74,8 +73,7 @@ module Rails
# the configuration.
#
# If you decide to define rake tasks, runners, or initializers in an
- # application other than +Rails.application+, then you must run those
- # these manually.
+ # application other than +Rails.application+, then you must run them manually.
class Application < Engine
autoload :Bootstrap, 'rails/application/bootstrap'
autoload :Configuration, 'rails/application/configuration'
@@ -113,7 +111,7 @@ module Rails
attr_accessor :assets, :sandbox
alias_method :sandbox?, :sandbox
- attr_reader :reloaders
+ attr_reader :reloaders, :reloader, :executor
delegate :default_url_options, :default_url_options=, to: :routes
@@ -131,6 +129,10 @@ module Rails
@message_verifiers = {}
@ran_load_hooks = false
+ @executor = Class.new(ActiveSupport::Executor)
+ @reloader = Class.new(ActiveSupport::Reloader)
+ @reloader.executor = @executor
+
# are these actually used?
@initial_variable_values = initial_variable_values
@block = block
@@ -214,7 +216,7 @@ module Rails
# url: http://localhost:3001
# namespace: my_app_development
#
- # # config/production.rb
+ # # config/environments/production.rb
# Rails.application.configure do
# config.middleware.use ExceptionNotifier, config_for(:exception_notification)
# end
@@ -243,7 +245,7 @@ module Rails
@app_env_config ||= begin
validate_secret_key_config!
- super.merge({
+ super.merge(
"action_dispatch.parameter_filter" => config.filter_parameters,
"action_dispatch.redirect_filter" => config.filter_redirect,
"action_dispatch.secret_token" => secrets.secret_token,
@@ -259,7 +261,7 @@ module Rails
"action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,
"action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest
- })
+ )
end
end
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 9baf8aa742..f615f22b26 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -1,6 +1,7 @@
-require "active_support/notifications"
-require "active_support/dependencies"
-require "active_support/descendants_tracker"
+require 'fileutils'
+require 'active_support/notifications'
+require 'active_support/dependencies'
+require 'active_support/descendants_tracker'
module Rails
class Application
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index 65cff1561a..f415a20833 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -58,7 +58,7 @@ module Rails
def static_cache_control=(value)
ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `static_cache_control` is deprecated and will be removed in Rails 5.1.
+ `config.static_cache_control` is deprecated and will be removed in Rails 5.1.
Please use
`config.public_file_server.headers = { 'Cache-Control' => '#{value}' }`
instead.
@@ -69,8 +69,8 @@ module Rails
def serve_static_files
ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `serve_static_files` is deprecated and will be removed in Rails 5.1.
- Please use `public_file_server.enabled` instead.
+ `config.serve_static_files` is deprecated and will be removed in Rails 5.1.
+ Please use `config.public_file_server.enabled` instead.
eow
@public_file_server.enabled
@@ -78,8 +78,8 @@ module Rails
def serve_static_files=(value)
ActiveSupport::Deprecation.warn <<-eow.strip_heredoc
- `serve_static_files` is deprecated and will be removed in Rails 5.1.
- Please use `public_file_server.enabled = #{value}` instead.
+ `config.serve_static_files` is deprecated and will be removed in Rails 5.1.
+ Please use `config.public_file_server.enabled = #{value}` instead.
eow
@public_file_server.enabled = value
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
index 4f1cc0703d..381e548730 100644
--- a/railties/lib/rails/application/default_middleware_stack.rb
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -34,22 +34,10 @@ module Rails
# handling: presumably their code is not threadsafe
middleware.use ::Rack::Lock
-
- elsif config.allow_concurrency == :unsafe
- # Do nothing, even if we know this is dangerous. This is the
- # historical behaviour for true.
-
- else
- # Default concurrency setting: enabled, but safe
-
- unless config.cache_classes && config.eager_load
- # Without cache_classes + eager_load, the load interlock
- # is required for proper operation
-
- middleware.use ::ActionDispatch::LoadInterlock
- end
end
+ middleware.use ::ActionDispatch::Executor, app.executor
+
middleware.use ::Rack::Runtime
middleware.use ::Rack::MethodOverride unless config.api_only
middleware.use ::ActionDispatch::RequestId
@@ -61,7 +49,7 @@ module Rails
middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
unless config.cache_classes
- middleware.use ::ActionDispatch::Reloader, lambda { reload_dependencies? }
+ middleware.use ::ActionDispatch::Reloader, app.reloader
end
middleware.use ::ActionDispatch::Callbacks
@@ -83,10 +71,6 @@ module Rails
private
- def reload_dependencies?
- config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
- end
-
def load_rack_cache
rack_cache = config.action_dispatch.rack_cache
return unless rack_cache
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index 411cdbad19..0aed6c1351 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -22,10 +22,10 @@ module Rails
initializer :add_builtin_route do |app|
if Rails.env.development?
app.routes.append do
- get '/rails/info/properties' => "rails/info#properties"
- get '/rails/info/routes' => "rails/info#routes"
- get '/rails/info' => "rails/info#index"
- get '/' => "rails/welcome#index"
+ get '/rails/info/properties' => "rails/info#properties", internal: true
+ get '/rails/info/routes' => "rails/info#routes", internal: true
+ get '/rails/info' => "rails/info#index", internal: true
+ get '/' => "rails/welcome#index", internal: true
end
end
end
@@ -38,16 +38,16 @@ module Rails
app.routes.define_mounted_helper(:main_app)
end
- initializer :add_to_prepare_blocks do
+ initializer :add_to_prepare_blocks do |app|
config.to_prepare_blocks.each do |block|
- ActionDispatch::Reloader.to_prepare(&block)
+ app.reloader.to_prepare(&block)
end
end
# This needs to happen before eager load so it happens
# in exactly the same point regardless of config.cache_classes
- initializer :run_prepare_callbacks do
- ActionDispatch::Reloader.prepare!
+ initializer :run_prepare_callbacks do |app|
+ app.reloader.prepare!
end
initializer :eager_load! do
@@ -62,13 +62,60 @@ module Rails
ActiveSupport.run_load_hooks(:after_initialize, self)
end
+ class MutexHook
+ def initialize(mutex = Mutex.new)
+ @mutex = mutex
+ end
+
+ def run
+ @mutex.lock
+ end
+
+ def complete(_state)
+ @mutex.unlock
+ end
+ end
+
+ module InterlockHook
+ def self.run
+ ActiveSupport::Dependencies.interlock.start_running
+ end
+
+ def self.complete(_state)
+ ActiveSupport::Dependencies.interlock.done_running
+ end
+ end
+
+ initializer :configure_executor_for_concurrency do |app|
+ if config.allow_concurrency == false
+ # User has explicitly opted out of concurrent request
+ # handling: presumably their code is not threadsafe
+
+ app.executor.register_hook(MutexHook.new, outer: true)
+
+ elsif config.allow_concurrency == :unsafe
+ # Do nothing, even if we know this is dangerous. This is the
+ # historical behaviour for true.
+
+ else
+ # Default concurrency setting: enabled, but safe
+
+ unless config.cache_classes && config.eager_load
+ # Without cache_classes + eager_load, the load interlock
+ # is required for proper operation
+
+ app.executor.register_hook(InterlockHook, outer: true)
+ end
+ end
+ end
+
# Set routes reload after the finisher hook to ensure routes added in
# the hook are taken into account.
- initializer :set_routes_reloader_hook do
+ initializer :set_routes_reloader_hook do |app|
reloader = routes_reloader
reloader.execute_if_updated
self.reloaders << reloader
- ActionDispatch::Reloader.to_prepare do
+ app.reloader.to_run do
# We configure #execute rather than #execute_if_updated because if
# autoloaded constants are cleared we need to reload routes also in
# case any was used there, as in
@@ -78,18 +125,27 @@ module Rails
# This means routes are also reloaded if i18n is updated, which
# might not be necessary, but in order to be more precise we need
# some sort of reloaders dependency support, to be added.
+ require_unload_lock!
reloader.execute
end
end
# Set clearing dependencies after the finisher hook to ensure paths
# added in the hook are taken into account.
- initializer :set_clear_dependencies_hook, group: :all do
+ initializer :set_clear_dependencies_hook, group: :all do |app|
callback = lambda do
- ActiveSupport::Dependencies.interlock.unloading do
- ActiveSupport::DescendantsTracker.clear
- ActiveSupport::Dependencies.clear
+ ActiveSupport::DescendantsTracker.clear
+ ActiveSupport::Dependencies.clear
+ end
+
+ if config.cache_classes
+ app.reloader.check = lambda { false }
+ elsif config.reload_classes_only_on_change
+ app.reloader.check = lambda do
+ app.reloaders.map(&:updated?).any?
end
+ else
+ app.reloader.check = lambda { true }
end
if config.reload_classes_only_on_change
@@ -99,15 +155,19 @@ module Rails
# Prepend this callback to have autoloaded constants cleared before
# any other possible reloading, in case they need to autoload fresh
# constants.
- ActionDispatch::Reloader.to_prepare(prepend: true) do
+ app.reloader.to_run(prepend: true) do
# In addition to changes detected by the file watcher, if routes
# or i18n have been updated we also need to clear constants,
# that's why we run #execute rather than #execute_if_updated, this
# callback has to clear autoloaded constants after any update.
- reloader.execute
+ class_unload! do
+ reloader.execute
+ end
end
else
- ActionDispatch::Reloader.to_cleanup(&callback)
+ app.reloader.to_complete do
+ class_unload!(&callback)
+ end
end
end
diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb
index fc8717c752..7a8f42fe94 100644
--- a/railties/lib/rails/code_statistics.rb
+++ b/railties/lib/rails/code_statistics.rb
@@ -1,4 +1,5 @@
require 'rails/code_statistics_calculator'
+require 'active_support/core_ext/enumerable'
class CodeStatistics #:nodoc:
diff --git a/railties/lib/rails/command.rb b/railties/lib/rails/command.rb
deleted file mode 100644
index f7753cbb83..0000000000
--- a/railties/lib/rails/command.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-require 'rails/commands/commands_tasks'
-
-module Rails
- class Command #:nodoc:
- attr_reader :argv
-
- def initialize(argv = [])
- @argv = argv
-
- @option_parser = build_option_parser
- @options = {}
- end
-
- def self.run(task_name, argv)
- command_name = command_name_for(task_name)
-
- if command = command_for(command_name)
- command.new(argv).run(command_name)
- else
- Rails::CommandsTasks.new(argv).run_command!(task_name)
- end
- end
-
- def run(command_name)
- parse_options_for(command_name)
- @option_parser.parse! @argv
-
- public_send(command_name)
- end
-
- def self.options_for(command_name, &options_to_parse)
- @@command_options[command_name] = options_to_parse
- end
-
- def self.set_banner(command_name, banner)
- options_for(command_name) { |opts, _| opts.banner = banner }
- end
-
- private
- @@commands = []
- @@command_options = {}
-
- def parse_options_for(command_name)
- @@command_options.fetch(command_name, proc {}).call(@option_parser, @options)
- end
-
- def build_option_parser
- OptionParser.new do |opts|
- opts.on('-h', '--help', 'Show this help.') do
- puts opts
- exit
- end
- end
- end
-
- def self.inherited(command)
- @@commands << command
- end
-
- def self.command_name_for(task_name)
- task_name.gsub(':', '_').to_sym
- end
-
- def self.command_for(command_name)
- @@commands.find do |command|
- command.public_instance_methods.include?(command_name)
- end
- end
- end
-end
diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb
index fa47c52b96..5a66b78a92 100644
--- a/railties/lib/rails/commands.rb
+++ b/railties/lib/rails/commands.rb
@@ -13,6 +13,6 @@ aliases = {
command = ARGV.shift
command = aliases[command] || command
-require 'rails/command'
+require 'rails/commands/commands_tasks'
-Rails::Command.run(command, ARGV)
+Rails::CommandsTasks.new(ARGV).run_command!(command)
diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb
index 5844e9037c..f9c183ac86 100644
--- a/railties/lib/rails/commands/runner.rb
+++ b/railties/lib/rails/commands/runner.rb
@@ -2,6 +2,7 @@ require 'optparse'
options = { environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup }
code_or_file = nil
+command = 'bin/rails runner'
if ARGV.first.nil?
ARGV.push "-h"
@@ -34,7 +35,7 @@ ARGV.clone.options do |opts|
opts.separator ""
opts.separator "You can also use runner as a shebang line for your executables:"
opts.separator " -------------------------------------------------------------"
- opts.separator " #!/usr/bin/env #{File.expand_path($0)} runner"
+ opts.separator " #!/usr/bin/env #{File.expand_path(command)}"
opts.separator ""
opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }"
opts.separator " -------------------------------------------------------------"
@@ -52,7 +53,7 @@ Rails.application.require_environment!
Rails.application.load_runner
if code_or_file.nil?
- $stderr.puts "Run '#{$0} -h' for help."
+ $stderr.puts "Run '#{command} -h' for help."
exit 1
elsif File.exist?(code_or_file)
$0 = code_or_file
@@ -62,7 +63,7 @@ else
eval(code_or_file, binding, __FILE__, __LINE__)
rescue SyntaxError, NameError
$stderr.puts "Please specify a valid ruby command or the path of a script to run."
- $stderr.puts "Run '#{$0} -h' for help."
+ $stderr.puts "Run '#{command} -h' for help."
exit 1
end
end
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
index 27cbaf360a..7418dff18b 100644
--- a/railties/lib/rails/commands/server.rb
+++ b/railties/lib/rails/commands/server.rb
@@ -2,6 +2,7 @@ require 'fileutils'
require 'optparse'
require 'action_dispatch'
require 'rails'
+require 'rails/dev_caching'
module Rails
class Server < ::Rack::Server
@@ -92,20 +93,17 @@ module Rails
DoNotReverseLookup: true,
environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup,
daemonize: false,
- caching: false,
- pid: Options::DEFAULT_PID_PATH
+ caching: nil,
+ pid: Options::DEFAULT_PID_PATH,
+ restart_cmd: restart_command
})
end
private
def setup_dev_caching
- return unless options[:environment] == "development"
-
- if options[:caching] == false
- delete_cache_file
- elsif options[:caching]
- create_cache_file
+ if options[:environment] == "development"
+ Rails::DevCaching.enable_by_argument(options[:caching])
end
end
@@ -114,16 +112,6 @@ module Rails
puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
puts "=> Run `rails server -h` for more startup options"
-
- puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
- end
-
- def create_cache_file
- FileUtils.touch("tmp/caching-dev.txt")
- end
-
- def delete_cache_file
- FileUtils.rm("tmp/caching-dev.txt") if File.exist?("tmp/caching-dev.txt")
end
def create_tmp_directories
@@ -143,5 +131,9 @@ module Rails
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
end
+
+ def restart_command
+ "bin/rails server #{ARGV.join(' ')}"
+ end
end
end
diff --git a/railties/lib/rails/console/app.rb b/railties/lib/rails/console/app.rb
index ac5836a588..9ad77e0a80 100644
--- a/railties/lib/rails/console/app.rb
+++ b/railties/lib/rails/console/app.rb
@@ -29,8 +29,7 @@ module Rails
# reloads the environment
def reload!(print=true)
puts "Reloading..." if print
- ActionDispatch::Reloader.cleanup!
- ActionDispatch::Reloader.prepare!
+ Rails.application.reloader.reload!
true
end
end
diff --git a/railties/lib/rails/dev_caching.rb b/railties/lib/rails/dev_caching.rb
new file mode 100644
index 0000000000..f2a53d6417
--- /dev/null
+++ b/railties/lib/rails/dev_caching.rb
@@ -0,0 +1,43 @@
+require 'fileutils'
+
+module Rails
+ module DevCaching # :nodoc:
+ class << self
+ FILE = 'tmp/caching-dev.txt'
+
+ def enable_by_file
+ FileUtils.mkdir_p('tmp')
+
+ if File.exist?(FILE)
+ delete_cache_file
+ puts 'Development mode is no longer being cached.'
+ else
+ create_cache_file
+ puts 'Development mode is now being cached.'
+ end
+
+ FileUtils.touch 'tmp/restart.txt'
+ FileUtils.rm_f('tmp/pids/server.pid')
+ end
+
+ def enable_by_argument(caching)
+ FileUtils.mkdir_p('tmp')
+
+ if caching
+ create_cache_file
+ elsif caching == false && File.exist?(FILE)
+ delete_cache_file
+ end
+ end
+
+ private
+ def create_cache_file
+ FileUtils.touch FILE
+ end
+
+ def delete_cache_file
+ File.delete FILE
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb
index 8cadbc3ddd..294d07446f 100644
--- a/railties/lib/rails/engine/configuration.rb
+++ b/railties/lib/rails/engine/configuration.rb
@@ -39,6 +39,7 @@ module Rails
paths.add "app", eager_load: true, glob: "{*,*/concerns}"
paths.add "app/assets", glob: "*"
paths.add "app/controllers", eager_load: true
+ paths.add "app/channels", eager_load: true, glob: "**/*_channel.rb"
paths.add "app/helpers", eager_load: true
paths.add "app/models", eager_load: true
paths.add "app/mailers", eager_load: true
diff --git a/railties/lib/rails/gem_version.rb b/railties/lib/rails/gem_version.rb
index 93e0151602..081222425c 100644
--- a/railties/lib/rails/gem_version.rb
+++ b/railties/lib/rails/gem_version.rb
@@ -8,7 +8,7 @@ module Rails
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index e3d79521e7..330bd7ec5d 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -105,7 +105,7 @@ module Rails
# Configure generators for API only applications. It basically hides
# everything that is usually browser related, such as assets and session
- # migration generators, and completely disable views, helpers and assets
+ # migration generators, and completely disable helpers and assets
# so generators such as scaffold won't create them.
def self.api_only!
hide_namespaces "assets", "helper", "css", "js"
@@ -116,6 +116,10 @@ module Rails
helper: false,
template_engine: nil
)
+
+ if ARGV.first == 'mailer'
+ options[:rails].merge!(template_engine: :erb)
+ end
end
# Remove the color from output.
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 9ca731347a..c947c062fa 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -1,5 +1,3 @@
-require 'open-uri'
-
module Rails
module Generators
module Actions
@@ -207,16 +205,22 @@ module Rails
in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) }
end
- # Runs the supplied rake task
+ # Runs the supplied rake task (invoked with 'rake ...')
#
# rake("db:migrate")
# rake("db:migrate", env: "production")
# rake("gems:install", sudo: true)
def rake(command, options={})
- log :rake, command
- env = options[:env] || ENV["RAILS_ENV"] || 'development'
- sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : ''
- in_root { run("#{sudo}#{extify(:rake)} #{command} RAILS_ENV=#{env}", verbose: false) }
+ execute_command :rake, command, options
+ end
+
+ # Runs the supplied rake task (invoked with 'rails ...')
+ #
+ # rails("db:migrate")
+ # rails("db:migrate", env: "production")
+ # rails("gems:install", sudo: true)
+ def rails_command(command, options={})
+ execute_command :rails, command, options
end
# Just run the capify command in root
@@ -270,6 +274,16 @@ module Rails
end
end
+
+ # Runs the supplied command using either "rake ..." or "rails ..."
+ # based on the executor parameter provided.
+ def execute_command(executor, command, options={})
+ log executor, command
+ env = options[:env] || ENV["RAILS_ENV"] || 'development'
+ sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : ''
+ in_root { run("#{sudo}#{extify(executor)} #{command} RAILS_ENV=#{env}", verbose: false) }
+ end
+
# Add an extension to the given name based on the platform.
def extify(name)
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb
index d664b07652..6c5b55466d 100644
--- a/railties/lib/rails/generators/actions/create_migration.rb
+++ b/railties/lib/rails/generators/actions/create_migration.rb
@@ -1,3 +1,4 @@
+require 'fileutils'
require 'thor/actions'
module Rails
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index 8f8c2ec9e1..151bf9a879 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -1,3 +1,4 @@
+require 'fileutils'
require 'digest/md5'
require 'active_support/core_ext/string/strip'
require 'rails/version' unless defined?(Rails::VERSION)
@@ -63,6 +64,9 @@ module Rails
class_option :skip_spring, type: :boolean, default: false,
desc: "Don't install Spring application preloader"
+ class_option :skip_listen, type: :boolean, default: false,
+ desc: "Don't generate configuration that depends on the listen gem"
+
class_option :skip_javascript, type: :boolean, aliases: '-J', default: false,
desc: 'Skip JavaScript files'
@@ -178,7 +182,7 @@ module Rails
def webserver_gemfile_entry
return [] if options[:skip_puma]
comment = 'Use Puma as the app server'
- GemfileEntry.new('puma', nil, comment)
+ GemfileEntry.new('puma', '~> 3.0', comment)
end
def include_all_railties?
@@ -308,12 +312,7 @@ module Rails
end
def coffee_gemfile_entry
- comment = 'Use CoffeeScript for .coffee assets and views'
- if options.dev? || options.edge?
- GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', nil, comment
- else
- GemfileEntry.version 'coffee-rails', '~> 4.1.0', comment
- end
+ GemfileEntry.version 'coffee-rails', '~> 4.1.0', 'Use CoffeeScript for .coffee assets and views'
end
def javascript_gemfile_entry
@@ -325,8 +324,8 @@ module Rails
"Use #{options[:javascript]} as the JavaScript library")
unless options[:skip_turbolinks]
- gems << GemfileEntry.version("turbolinks", nil,
- "Turbolinks makes following links in your web application faster. Read more: https://github.com/turbolinks/turbolinks")
+ gems << GemfileEntry.version("turbolinks", "~> 5.x",
+ "Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks")
end
gems
@@ -390,6 +389,10 @@ module Rails
!options[:skip_spring] && !options.dev? && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin")
end
+ def depend_on_listen?
+ !options[:skip_listen] && os_supports_listen_out_of_the_box?
+ end
+
def os_supports_listen_out_of_the_box?
RbConfig::CONFIG['host_os'] =~ /darwin|linux/
end
diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
index bc249aa5e5..7f00943d80 100644
--- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
@@ -26,7 +26,7 @@ module Erb # :nodoc:
end
def file_name
- @_file_name ||= super.gsub(/\_mailer/i, '')
+ @_file_name ||= super.gsub(/_mailer/i, '')
end
end
end
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index 248ad20019..f58e6ba653 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -80,6 +80,7 @@ module Rails
template "secrets.yml"
template "cable.yml" unless options[:skip_action_cable]
template "puma.rb" unless options[:skip_puma]
+ template "spring.rb" if spring_install?
directory "environments"
directory "initializers"
@@ -91,20 +92,37 @@ module Rails
cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb')
callback_terminator_config_exist = File.exist?('config/initializers/callback_terminator.rb')
active_record_belongs_to_required_by_default_config_exist = File.exist?('config/initializers/active_record_belongs_to_required_by_default.rb')
+ action_cable_config_exist = File.exist?('config/cable.yml')
+ ssl_options_exist = File.exist?('config/initializers/ssl_options.rb')
+ rack_cors_config_exist = File.exist?('config/initializers/cors.rb')
config
+ gsub_file 'config/environments/development.rb', /^(\s+)config\.file_watcher/, '\1# config.file_watcher'
+
unless callback_terminator_config_exist
remove_file 'config/initializers/callback_terminator.rb'
end
unless cookie_serializer_config_exist
- gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal'
+ gsub_file 'config/initializers/cookies_serializer.rb', /json(?!,)/, 'marshal'
end
unless active_record_belongs_to_required_by_default_config_exist
remove_file 'config/initializers/active_record_belongs_to_required_by_default.rb'
end
+
+ unless action_cable_config_exist
+ template 'config/cable.yml'
+ end
+
+ unless ssl_options_exist
+ remove_file 'config/initializers/ssl_options.rb'
+ end
+
+ unless rack_cors_config_exist
+ remove_file 'config/initializers/cors.rb'
+ end
end
def database_yml
@@ -276,9 +294,9 @@ module Rails
end
end
- def delete_app_views_if_api_option
+ def delete_application_layout_file_if_api_option
if options[:api]
- remove_dir 'app/views'
+ remove_file 'app/views/layouts/application.html.erb'
end
end
@@ -317,7 +335,7 @@ module Rails
def delete_action_cable_files_skipping_action_cable
if options[:skip_action_cable]
remove_file 'config/cable.yml'
- remove_file 'app/assets/javascripts/cable.coffee'
+ remove_file 'app/assets/javascripts/cable.js'
remove_dir 'app/channels'
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index c3fad31f23..86143ca1f1 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -26,25 +26,25 @@ source 'https://rubygems.org'
<% if RUBY_ENGINE == 'ruby' -%>
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
- gem 'byebug'
+ gem 'byebug', platform: :mri
end
group :development do
<%- unless options.api? -%>
- # Access an IRB console on exception pages or by using <%%= console %> in views
+ # Access an IRB console on exception pages or by using <%%= console %> anywhere in the code.
<%- if options.dev? || options.edge? -%>
gem 'web-console', github: 'rails/web-console'
<%- else -%>
- gem 'web-console', '~> 3.0'
+ gem 'web-console'
<%- end -%>
<%- end -%>
-<% if os_supports_listen_out_of_the_box? -%>
+<% if depend_on_listen? -%>
gem 'listen', '~> 3.0.5'
<% end -%>
<% if spring_install? -%>
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'spring'
-<% if os_supports_listen_out_of_the_box? -%>
+<% if depend_on_listen? -%>
gem 'spring-watcher-listen', '~> 2.0.0'
<% end -%>
<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/README.md b/railties/lib/rails/generators/rails/app/templates/README.md
index 55e144da18..7db80e4ca1 100644
--- a/railties/lib/rails/generators/rails/app/templates/README.md
+++ b/railties/lib/rails/generators/rails/app/templates/README.md
@@ -1,4 +1,4 @@
-## README
+# README
This README would normally document whatever steps are necessary to get the
application up and running.
diff --git a/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile
index ba6b733dd2..e85f913914 100644
--- a/railties/lib/rails/generators/rails/app/templates/Rakefile
+++ b/railties/lib/rails/generators/rails/app/templates/Rakefile
@@ -1,6 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
-require File.expand_path('../config/application', __FILE__)
+require_relative 'config/application'
Rails.application.load_tasks
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee
deleted file mode 100644
index 07934d026f..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee
+++ /dev/null
@@ -1,11 +0,0 @@
-# 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.
-#
-# Turn on the cable connection by removing the comments after the require statements (and ensure it's also on in config/routes.rb).
-#
-#= require action_cable
-#= require_self
-#= require_tree ./channels
-#
-# @App ||= {}
-# App.cable = ActionCable.createConsumer()
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js
new file mode 100644
index 0000000000..71ee1e66de
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.js
@@ -0,0 +1,13 @@
+// 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.
+//
+//= require action_cable
+//= require_self
+//= require_tree ./channels
+
+(function() {
+ this.App || (this.App = {});
+
+ App.cable = ActionCable.createConsumer();
+
+}).call(this);
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
index 68b5c051b2..d51f79bd49 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
@@ -3,16 +3,13 @@
<head>
<title><%= camelized %></title>
<%%= csrf_meta_tags %>
- <%- unless options[:skip_action_cable] -%>
- <%%= action_cable_meta_tag %>
- <%- end -%>
<%- if options[:skip_javascript] -%>
<%%= stylesheet_link_tag 'application', media: 'all' %>
<%- else -%>
<%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%>
- <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
- <%%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
+ <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
+ <%%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
<%- else -%>
<%%= stylesheet_link_tag 'application', media: 'all' %>
<%%= javascript_include_tag 'application' %>
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails b/railties/lib/rails/generators/rails/app/templates/bin/rails
index 80ec8080ab..513a2e0183 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/rails
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rails
@@ -1,3 +1,3 @@
-APP_PATH = File.expand_path('../../config/application', __FILE__)
+APP_PATH = File.expand_path('../config/application', __dir__)
require_relative '../config/boot'
require 'rails/commands'
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup
index df88bfd3bc..acae810c1a 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/setup
+++ b/railties/lib/rails/generators/rails/app/templates/bin/setup
@@ -15,7 +15,7 @@ chdir APP_ROOT do
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
- system('bundle check') or system!('bundle install')
+ system('bundle check') || system!('bundle install')
# puts "\n== Copying sample files =="
# unless File.exist?('config/database.yml')
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/update b/railties/lib/rails/generators/rails/app/templates/bin/update
index c6ed3ae64b..770a605fed 100644
--- a/railties/lib/rails/generators/rails/app/templates/bin/update
+++ b/railties/lib/rails/generators/rails/app/templates/bin/update
@@ -15,7 +15,7 @@ chdir APP_ROOT do
puts '== Installing dependencies =='
system! 'gem install bundler --conservative'
- system 'bundle check' or system! 'bundle install'
+ system('bundle check') || system!('bundle install')
puts "\n== Updating database =="
system! 'bin/rails db:migrate'
diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru
new file mode 100644
index 0000000000..f7ba0b527b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config.ru
@@ -0,0 +1,5 @@
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru.tt b/railties/lib/rails/generators/rails/app/templates/config.ru.tt
deleted file mode 100644
index 343c0833d7..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config.ru.tt
+++ /dev/null
@@ -1,10 +0,0 @@
-# This file is used by Rack-based servers to start the application.
-
-require ::File.expand_path('../config/environment', __FILE__)
-<%- unless options[:skip_action_cable] -%>
-
-# Action Cable requires that all classes are loaded in advance
-Rails.application.eager_load!
-<%- end -%>
-
-run Rails.application
diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb
index cb83364360..c0a0bd0a3e 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/application.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb
@@ -1,4 +1,4 @@
-require File.expand_path('../boot', __FILE__)
+require_relative 'boot'
<% if include_all_railties? -%>
require 'rails/all'
diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb
index 6b750f00b1..30f5120df6 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb
@@ -1,3 +1,3 @@
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb
index ee8d90dc65..426333bb46 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb
@@ -1,5 +1,5 @@
# Load the Rails application.
-require File.expand_path('../application', __FILE__)
+require_relative 'application'
# Initialize the Rails application.
Rails.application.initialize!
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
index 3451ade158..7a537610e9 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt
@@ -15,18 +15,22 @@ Rails.application.configure do
# Enable/disable caching. By default caching is disabled.
if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
+
config.cache_store = :memory_store
config.public_file_server.headers = {
'Cache-Control' => 'public, max-age=172800'
}
else
config.action_controller.perform_caching = false
+
config.cache_store = :null_store
end
<%- unless options.skip_action_mailer? -%>
# Don't care if the mailer can't send.
config.action_mailer.raise_delivery_errors = false
+
+ config.action_mailer.perform_caching = false
<%- end -%>
# Print deprecation notices to the Rails logger.
@@ -42,15 +46,6 @@ Rails.application.configure do
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = true
-
- # Asset digests allow you to set far-future HTTP expiration dates on all assets,
- # yet still be able to expire them through the digest params.
- config.assets.digest = true
-
- # Adds additional error checking when serving assets at runtime.
- # Checks for improperly declared sprockets dependencies.
- # Raises helpful error messages.
- config.assets.raise_runtime_errors = true
<%- end -%>
# Raises error for missing translations
@@ -58,5 +53,5 @@ Rails.application.configure do
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
- <%= '# ' unless os_supports_listen_out_of_the_box? %>config.file_watcher = ActiveSupport::EventedFileUpdateChecker
+ <%= '# ' unless depend_on_listen? %>config.file_watcher = ActiveSupport::EventedFileUpdateChecker
end
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 82509f5ef5..6bd5e42251 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
@@ -26,10 +26,6 @@ Rails.application.configure do
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
- # Asset digests allow you to set far-future HTTP expiration dates on all assets,
- # yet still be able to expire them through the digest params.
- config.assets.digest = true
-
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
<%- end -%>
@@ -44,6 +40,9 @@ Rails.application.configure do
# Action Cable endpoint configuration
# config.action_cable.url = 'wss://example.com/cable'
# config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
+
+ # Don't mount Action Cable in the main server process.
+ # config.action_cable.mount_path = nil
<%- end -%>
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
@@ -56,10 +55,6 @@ Rails.application.configure do
# Prepend all log lines with the following tags.
config.log_tags = [ :request_id ]
- # Use a different logger for distributed setups.
- # require 'syslog/logger'
- # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
-
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
@@ -67,6 +62,7 @@ Rails.application.configure do
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}"
<%- unless options.skip_action_mailer? -%>
+ config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
@@ -82,6 +78,16 @@ Rails.application.configure do
# Use default logging formatter so that PID and timestamp are not suppressed.
config.log_formatter = ::Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require 'syslog/logger'
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
+
+ if ENV["RAILS_LOG_TO_STDOUT"].present?
+ logger = ActiveSupport::Logger.new(STDOUT)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
<%- unless options.skip_active_record? -%>
# Do not dump schema after migrations.
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 8133917591..42fee3b036 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
@@ -28,6 +28,7 @@ Rails.application.configure do
# Disable request forgery protection in test environment.
config.action_controller.allow_forgery_protection = false
<%- unless options.skip_action_mailer? -%>
+ config.action_mailer.perform_caching = false
# Tell Action Mailer not to deliver emails to the real world.
# The :test delivery method accumulates sent emails in the
@@ -35,9 +36,6 @@ Rails.application.configure do
config.action_mailer.delivery_method = :test
<%- end -%>
- # Randomize the order test cases are executed.
- config.active_support.test_order = :random
-
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb
new file mode 100644
index 0000000000..1775dea1e7
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/ssl_options.rb
@@ -0,0 +1,4 @@
+# Be sure to restart your server when you modify this file.
+
+# Configure SSL options to enable HSTS with subdomains.
+Rails.application.config.ssl_options = { hsts: { subdomains: true } }
diff --git a/railties/lib/rails/generators/rails/app/templates/config/puma.rb b/railties/lib/rails/generators/rails/app/templates/config/puma.rb
index 1bf274bc66..c7f311f811 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/puma.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/puma.rb
@@ -42,3 +42,6 @@ environment ENV.fetch("RAILS_ENV") { "development" }
# on_worker_boot do
# ActiveRecord::Base.establish_connection if defined?(ActiveRecord)
# end
+
+# Allow puma to be restarted by `rails restart` command.
+plugin :tmp_restart
diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb
index 8293c8a483..787824f888 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb
@@ -1,6 +1,3 @@
Rails.application.routes.draw do
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
-
- # Serve websocket cable requests in-process
- # mount ActionCable.server => '/cable'
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/spring.rb b/railties/lib/rails/generators/rails/app/templates/config/spring.rb
new file mode 100644
index 0000000000..c9119b40c0
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/spring.rb
@@ -0,0 +1,6 @@
+%w(
+ .ruby-version
+ .rbenv-vars
+ tmp/restart.txt
+ tmp/caching-dev.txt
+).each { |path| Spring.watch(path) }
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
index b5e836b584..56efd35a95 100644
--- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -90,6 +90,7 @@ task default: :test
opts[:force] = force
opts[:skip_bundle] = true
opts[:api] = options.api?
+ opts[:skip_listen] = true
invoke Rails::Generators::AppGenerator,
[ File.expand_path(dummy_path, destination_root) ], opts
@@ -287,10 +288,6 @@ task default: :test
protected
- def app_templates_dir
- "../../app/templates"
- end
-
def create_dummy_app(path = nil)
dummy_path(path) if path
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
index 83807f14b4..abbacd9bec 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt
@@ -1,6 +1,6 @@
<%= wrap_in_modules <<-rb.strip_heredoc
class ApplicationController < ActionController::#{api? ? "API" : "Base"}
- #{ api? ? '# ' : '' }protect_from_forgery :with => :exception
+ #{ api? ? '# ' : '' }protect_from_forgery with: :exception
end
rb
%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
index b1038c839e..d03b1be878 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
@@ -1,4 +1,4 @@
-require File.expand_path('../boot', __FILE__)
+require_relative 'boot'
<% if include_all_railties? -%>
require 'rails/all'
@@ -6,10 +6,12 @@ require 'rails/all'
# Pick the frameworks you want:
<%= comment_if :skip_active_record %>require "active_record/railtie"
require "action_controller/railtie"
-<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
require "action_view/railtie"
-<%= comment_if :skip_sprockets %>require "sprockets/railtie"
+<%= comment_if :skip_action_mailer %>require "action_mailer/railtie"
+require "active_job/railtie"
+<%= comment_if :skip_action_cable %>require "action_cable/engine"
<%= comment_if :skip_test %>require "rails/test_unit/railtie"
+<%= comment_if :skip_sprockets %>require "sprockets/railtie"
<% end -%>
Bundler.require(*Rails.groups)
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
index 6266cfc509..c9aef85d40 100644
--- a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
@@ -1,5 +1,5 @@
# Set up gems listed in the Gemfile.
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__)
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
-$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
+$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__)
diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
index 343c8a3949..76a0b79654 100644
--- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
@@ -19,7 +19,7 @@ module TestUnit # :nodoc:
protected
def file_name
- @_file_name ||= super.gsub(/\_mailer/i, '')
+ @_file_name ||= super.gsub(/_mailer/i, '')
end
end
end
diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb
index 8c24d1d56d..492c519222 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -1,38 +1,37 @@
require 'rails/initializable'
-require 'rails/configuration'
require 'active_support/inflector'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/module/delegation'
module Rails
- # Railtie is the core of the Rails framework and provides several hooks to extend
- # Rails and/or modify the initialization process.
+ # <tt>Rails::Railtie</tt> is the core of the Rails framework and provides
+ # several hooks to extend Rails and/or modify the initialization process.
#
- # Every major component of Rails (Action Mailer, Action Controller,
- # Action View and Active Record) is a Railtie. Each of
- # them is responsible for their own initialization. This makes Rails itself
- # absent of any component hooks, allowing other components to be used in
- # place of any of the Rails defaults.
+ # Every major component of Rails (Action Mailer, Action Controller, Active
+ # Record, etc.) implements a railtie. Each of them is responsible for their
+ # own initialization. This makes Rails itself absent of any component hooks,
+ # allowing other components to be used in place of any of the Rails defaults.
#
- # Developing a Rails extension does _not_ require any implementation of
- # Railtie, but if you need to interact with the Rails framework during
- # or after boot, then Railtie is needed.
+ # Developing a Rails extension does _not_ require implementing a railtie, but
+ # if you need to interact with the Rails framework during or after boot, then
+ # a railtie is needed.
#
- # For example, an extension doing any of the following would require Railtie:
+ # For example, an extension doing any of the following would need a railtie:
#
# * creating initializers
# * configuring a Rails framework for the application, like setting a generator
# * adding <tt>config.*</tt> keys to the environment
- # * setting up a subscriber with ActiveSupport::Notifications
- # * adding rake tasks
+ # * setting up a subscriber with <tt>ActiveSupport::Notifications</tt>
+ # * adding Rake tasks
#
- # == Creating your Railtie
+ # == Creating a Railtie
#
- # To extend Rails using Railtie, create a Railtie class which inherits
- # from Rails::Railtie within your extension's namespace. This class must be
- # loaded during the Rails boot process.
+ # To extend Rails using a railtie, create a subclass of <tt>Rails::Railtie</tt>.
+ # This class must be loaded during the Rails boot process, and is conventionally
+ # called <tt>MyNamespace::Railtie</tt>.
#
- # The following example demonstrates an extension which can be used with or without Rails.
+ # The following example demonstrates an extension which can be used with or
+ # without Rails.
#
# # lib/my_gem/railtie.rb
# module MyGem
@@ -45,8 +44,8 @@ module Rails
#
# == Initializers
#
- # To add an initialization step from your Railtie to Rails boot process, you just need
- # to create an initializer block:
+ # To add an initialization step to the Rails boot process from your railtie, just
+ # define the initialization code with the +initializer+ macro:
#
# class MyRailtie < Rails::Railtie
# initializer "my_railtie.configure_rails_initialization" do
@@ -55,7 +54,7 @@ module Rails
# end
#
# If specified, the block can also receive the application object, in case you
- # need to access some application specific configuration, like middleware:
+ # need to access some application-specific configuration, like middleware:
#
# class MyRailtie < Rails::Railtie
# initializer "my_railtie.configure_rails_initialization" do |app|
@@ -63,56 +62,56 @@ module Rails
# end
# end
#
- # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as option to initializer,
- # in case you want to couple it with a specific step in the initialization process.
+ # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as options to
+ # +initializer+, in case you want to couple it with a specific step in the
+ # initialization process.
#
# == Configuration
#
- # Inside the Railtie class, you can access a config object which contains configuration
- # shared by all railties and the application:
+ # Railties can access a config object which contains configuration shared by all
+ # railties and the application:
#
# class MyRailtie < Rails::Railtie
# # Customize the ORM
# config.app_generators.orm :my_railtie_orm
#
# # Add a to_prepare block which is executed once in production
- # # and before each request in development
+ # # and before each request in development.
# config.to_prepare do
# MyRailtie.setup!
# end
# end
#
- # == Loading rake tasks and generators
+ # == Loading Rake Tasks and Generators
#
- # If your railtie has rake tasks, you can tell Rails to load them through the method
- # rake_tasks:
+ # If your railtie has Rake tasks, you can tell Rails to load them through the method
+ # +rake_tasks+:
#
# class MyRailtie < Rails::Railtie
# rake_tasks do
- # load "path/to/my_railtie.tasks"
+ # load 'path/to/my_railtie.tasks'
# end
# end
#
# By default, Rails loads generators from your load path. However, if you want to place
- # your generators at a different location, you can specify in your Railtie a block which
+ # your generators at a different location, you can specify in your railtie a block which
# will load them during normal generators lookup:
#
# class MyRailtie < Rails::Railtie
# generators do
- # require "path/to/my_railtie_generator"
+ # require 'path/to/my_railtie_generator'
# end
# end
#
# == Application and Engine
#
- # A Rails::Engine is nothing more than a Railtie with some initializers already set.
- # And since Rails::Application is an engine, the same configuration described here
- # can be used in both.
+ # An engine is nothing more than a railtie with some initializers already set. And since
+ # <tt>Rails::Application</tt> is an engine, the same configuration described here can be
+ # used in both.
#
# Be sure to look at the documentation of those specific classes for more information.
- #
class Railtie
- autoload :Configuration, "rails/railtie/configuration"
+ autoload :Configuration, 'rails/railtie/configuration'
include Initializable
@@ -183,7 +182,7 @@ module Rails
end
protected
- def generate_railtie_name(string)
+ def generate_railtie_name(string) #:nodoc:
ActiveSupport::Inflector.underscore(string).tr("/", "_")
end
@@ -200,21 +199,24 @@ module Rails
delegate :railtie_name, to: :class
- def initialize
+ def initialize #:nodoc:
if self.class.abstract_railtie?
raise "#{self.class.name} is abstract, you cannot instantiate it directly."
end
end
- def configure(&block)
+ def configure(&block) #:nodoc:
instance_eval(&block)
end
+ # This is used to create the <tt>config</tt> object on Railties, an instance of
+ # Railtie::Configuration, that is used by Railties and Application to store
+ # related configuration.
def config
@config ||= Railtie::Configuration.new
end
- def railtie_namespace
+ def railtie_namespace #:nodoc:
@railtie_namespace ||= self.class.parents.detect { |n| n.respond_to?(:railtie_namespace) }
end
diff --git a/railties/lib/rails/tasks/dev.rake b/railties/lib/rails/tasks/dev.rake
index 4593100465..d2ceaacc0c 100644
--- a/railties/lib/rails/tasks/dev.rake
+++ b/railties/lib/rails/tasks/dev.rake
@@ -1,14 +1,8 @@
+require 'rails/dev_caching'
+
namespace :dev do
desc 'Toggle development mode caching on/off'
task :cache do
- if File.exist? 'tmp/caching-dev.txt'
- File.delete 'tmp/caching-dev.txt'
- puts 'Development mode is no longer being cached.'
- else
- FileUtils.touch 'tmp/caching-dev.txt'
- puts 'Development mode is now being cached.'
- end
-
- FileUtils.touch 'tmp/restart.txt'
+ Rails::DevCaching.enable_by_file
end
end
diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake
index 7601836809..3e771167ee 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -1,4 +1,6 @@
-namespace :rails do
+require 'active_support/deprecation'
+
+namespace :app do
desc "Update configs and some other initially generated files (or use just update:configs or update:bin)"
task update: [ "update:configs", "update:bin" ]
@@ -24,12 +26,12 @@ namespace :rails do
default_templates.each do |type, names|
local_template_type_dir = File.join(project_templates, type)
- FileUtils.mkdir_p local_template_type_dir
+ mkdir_p local_template_type_dir, verbose: false
names.each do |name|
dst_name = File.join(local_template_type_dir, name)
src_name = File.join(generators_lib, type, name, "templates")
- FileUtils.cp_r src_name, dst_name
+ cp_r src_name, dst_name, verbose: false
end
end
end
@@ -45,8 +47,9 @@ namespace :rails do
@app_generator ||= begin
require 'rails/generators'
require 'rails/generators/rails/app/app_generator'
- gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true, api: !!Rails.application.config.api_only },
- destination_root: Rails.root
+ gen = Rails::Generators::AppGenerator.new ["rails"],
+ { api: !!Rails.application.config.api_only },
+ destination_root: Rails.root
File.exist?(Rails.root.join("config", "application.rb")) ?
gen.send(:app_const) : gen.send(:valid_const?)
gen
@@ -66,3 +69,15 @@ namespace :rails do
end
end
end
+
+namespace :rails do
+ %i(update template templates:copy update:configs update:bin).each do |task_name|
+ task "#{task_name}" do
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Running #{task_name} with the rails: namespace is deprecated in favor of app: namespace.
+ Run bin/rails app:#{task_name} instead.
+ MSG
+ Rake.application.invoke_task("app:#{task_name}")
+ end
+ end
+end
diff --git a/railties/lib/rails/tasks/restart.rake b/railties/lib/rails/tasks/restart.rake
index f36c86d81b..3f98cbe51f 100644
--- a/railties/lib/rails/tasks/restart.rake
+++ b/railties/lib/rails/tasks/restart.rake
@@ -1,5 +1,8 @@
-desc "Restart app by touching tmp/restart.txt"
+desc 'Restart app by touching tmp/restart.txt'
task :restart do
- FileUtils.mkdir_p('tmp')
- FileUtils.touch('tmp/restart.txt')
+ verbose(false) do
+ mkdir_p 'tmp'
+ touch 'tmp/restart.txt'
+ rm_f 'tmp/pids/server.pid'
+ end
end
diff --git a/railties/lib/rails/tasks/tmp.rake b/railties/lib/rails/tasks/tmp.rake
index 9162ef234a..c74a17a0ca 100644
--- a/railties/lib/rails/tasks/tmp.rake
+++ b/railties/lib/rails/tasks/tmp.rake
@@ -5,9 +5,7 @@ namespace :tmp do
tmp_dirs = [ 'tmp/cache',
'tmp/sockets',
'tmp/pids',
- 'tmp/cache/assets/development',
- 'tmp/cache/assets/test',
- 'tmp/cache/assets/production' ]
+ 'tmp/cache/assets' ]
tmp_dirs.each { |d| directory d }
@@ -17,21 +15,21 @@ namespace :tmp do
namespace :cache do
# desc "Clears all files and directories in tmp/cache"
task :clear do
- FileUtils.rm_rf(Dir['tmp/cache/[^.]*'])
+ rm_rf Dir['tmp/cache/[^.]*'], verbose: false
end
end
namespace :sockets do
# desc "Clears all files in tmp/sockets"
task :clear do
- FileUtils.rm(Dir['tmp/sockets/[^.]*'])
+ rm Dir['tmp/sockets/[^.]*'], verbose: false
end
end
namespace :pids do
# desc "Clears all files in tmp/pids"
task :clear do
- FileUtils.rm(Dir['tmp/pids/[^.]*'])
+ rm Dir['tmp/pids/[^.]*'], verbose: false
end
end
end
diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb
index 29a3d991b8..e9195d5b4e 100644
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ b/railties/lib/rails/test_unit/minitest_plugin.rb
@@ -1,6 +1,7 @@
require "active_support/core_ext/module/attribute_accessors"
require "rails/test_unit/reporter"
require "rails/test_unit/test_requirer"
+require 'shellwords'
module Minitest
class SuppressedSummaryReporter < SummaryReporter
@@ -42,7 +43,7 @@ module Minitest
end
opts.on("-f", "--fail-fast",
- "Abort test run on first failure") do
+ "Abort test run on first failure or error") do
options[:fail_fast] = true
end
@@ -60,11 +61,13 @@ module Minitest
# as the patterns would also contain the other Rake tasks.
def self.rake_run(patterns) # :nodoc:
@rake_patterns = patterns
- passed = run
+ passed = run(Shellwords.split(ENV['TESTOPTS'] || ''))
exit passed unless passed
passed
end
+ # Owes great inspiration to test runner trailblazers like RSpec,
+ # minitest-reporters, maxitest and others.
def self.plugin_rails_init(options)
self.run_with_rails_extension = true
@@ -81,14 +84,18 @@ module Minitest
end
# Replace progress reporter for colors.
- self.reporter.reporters.delete_if { |reporter| reporter.kind_of?(SummaryReporter) || reporter.kind_of?(ProgressReporter) }
- self.reporter << SuppressedSummaryReporter.new(options[:io], options)
- self.reporter << ::Rails::TestUnitReporter.new(options[:io], options)
+ reporter.reporters.delete_if { |reporter| reporter.kind_of?(SummaryReporter) || reporter.kind_of?(ProgressReporter) }
+ reporter << SuppressedSummaryReporter.new(options[:io], options)
+ reporter << ::Rails::TestUnitReporter.new(options[:io], options)
end
mattr_accessor(:run_with_autorun) { false }
mattr_accessor(:run_with_rails_extension) { false }
end
+# Put Rails as the first plugin minitest initializes so other plugins
+# can override or replace our default reporter setup.
+# Since minitest only loads plugins if its extensions are empty we have
+# to call `load_plugins` first.
Minitest.load_plugins
-Minitest.extensions << 'rails'
+Minitest.extensions.unshift 'rails'
diff --git a/railties/lib/rails/test_unit/reporter.rb b/railties/lib/rails/test_unit/reporter.rb
index 73b8d7d27b..4086d5b731 100644
--- a/railties/lib/rails/test_unit/reporter.rb
+++ b/railties/lib/rails/test_unit/reporter.rb
@@ -18,13 +18,13 @@ module Rails
if output_inline? && result.failure && (!result.skipped? || options[:verbose])
io.puts
io.puts
- io.puts format_failures(result).map { |line| color_output(line, by: result) }
+ io.puts color_output(result, by: result)
io.puts
io.puts format_rerun_snippet(result)
io.puts
end
- if fail_fast? && result.failure && !result.error? && !result.skipped?
+ if fail_fast? && result.failure && !result.skipped?
raise Interrupt
end
end
@@ -66,21 +66,9 @@ module Rails
"%s#%s = %.2f s = %s" % [result.class, result.name, result.time, result.result_code]
end
- def format_failures(result)
- result.failures.map do |failure|
- "#{failure.result_label}:\n#{result.class}##{result.name}:\n#{failure.message}\n"
- end
- end
-
def format_rerun_snippet(result)
- # Try to extract path to assertion from backtrace.
- if result.location =~ /\[(.*)\]\z/
- assertion_path = $1
- else
- assertion_path = result.method(result.name).source_location.join(':')
- end
-
- "#{self.executable} #{relative_path_for(assertion_path)}"
+ location, line = result.method(result.name).source_location
+ "#{self.executable} #{relative_path_for(location)}:#{line}"
end
def app_root
diff --git a/railties/railties.gemspec b/railties/railties.gemspec
index a06336f698..fa417816eb 100644
--- a/railties/railties.gemspec
+++ b/railties/railties.gemspec
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.homepage = 'http://rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'RDOC_MAIN.rdoc', 'exe/**/*', 'lib/**/{*,.[a-z]*}']
s.require_path = 'lib'
diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index 794d180e5d..2a5a731fe2 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -1,7 +1,5 @@
ENV["RAILS_ENV"] ||= "test"
-require File.expand_path("../../../load_paths", __FILE__)
-
require 'stringio'
require 'active_support/testing/autorun'
require 'active_support/testing/stream'
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index 2670fad618..e32eea42b7 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -186,6 +186,26 @@ module ApplicationTests
assert_file_exists("#{app_path}/public/assets/something-*.js")
end
+ test 'sprockets cache is not shared between environments' do
+ app_file "app/assets/images/rails.png", "notactuallyapng"
+ app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
+ add_to_env_config 'production', 'config.assets.prefix = "production_assets"'
+
+ precompile!
+
+ assert_file_exists("#{app_path}/public/assets/application-*.css")
+
+ file = Dir["#{app_path}/public/assets/application-*.css"].first
+ assert_match(/assets\/rails-([0-z]+)\.png/, File.read(file))
+
+ precompile! RAILS_ENV: 'production'
+
+ assert_file_exists("#{app_path}/public/production_assets/application-*.css")
+
+ file = Dir["#{app_path}/public/production_assets/application-*.css"].first
+ assert_match(/production_assets\/rails-([0-z]+)\.png/, File.read(file))
+ end
+
test 'precompile use assets defined in app config and reassigned in app env config' do
add_to_config 'config.assets.precompile = [ "something_manifest.js" ]'
add_to_env_config 'production', 'config.assets.precompile += [ "another_manifest.js" ]'
@@ -324,8 +344,7 @@ module ApplicationTests
clean_assets!
- files = Dir["#{app_path}/public/assets/**/*", "#{app_path}/tmp/cache/assets/development/*",
- "#{app_path}/tmp/cache/assets/test/*", "#{app_path}/tmp/cache/assets/production/*"]
+ files = Dir["#{app_path}/public/assets/**/*"]
assert_equal 0, files.length, "Expected no assets, but found #{files.join(', ')}"
end
diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb
index a07c51a60f..ba700df1d6 100644
--- a/railties/test/application/bin_setup_test.rb
+++ b/railties/test/application/bin_setup_test.rb
@@ -43,6 +43,8 @@ module ApplicationTests
The Gemfile's dependencies are satisfied
== Preparing database ==
+Created database 'db/development.sqlite3'
+Created database 'db/test.sqlite3'
== Removing old logs and tempfiles ==
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index 7bcfc86d03..decc4d138d 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -348,6 +348,17 @@ module ApplicationTests
end
end
+ test "In production mode, STDOUT logging is enabled when RAILS_LOG_TO_STDOUT is set" do
+ restore_default_config
+
+ with_rails_env "production" do
+ switch_env "RAILS_LOG_TO_STDOUT", "1" do
+ app 'production'
+ assert ActiveSupport::Logger.logger_outputs_to?(app.config.logger, STDOUT)
+ end
+ end
+ end
+
test "In production mode, config.public_file_server.enabled is disabled when RAILS_SERVE_STATIC_FILES is blank" do
restore_default_config
@@ -988,7 +999,7 @@ module ApplicationTests
app 'development'
post "/posts.json", '{ "title": "foo", "name": "bar" }', "CONTENT_TYPE" => "application/json"
- assert_equal '{"title"=>"foo"}', last_response.body
+ assert_equal '<ActionController::Parameters {"title"=>"foo"} permitted: false>', last_response.body
end
test "config.action_controller.permit_all_parameters = true" do
@@ -1326,7 +1337,7 @@ module ApplicationTests
assert_equal 'custom key', Rails.application.config.my_custom_config['key']
end
- test "config_for use the Pathname object if it is provided" do
+ test "config_for uses the Pathname object if it is provided" do
app_file 'config/custom.yml', <<-RUBY
development:
key: 'custom key'
@@ -1444,7 +1455,7 @@ module ApplicationTests
assert Rails.configuration.api_only
end
- test "debug_exception_response_format is :api by default if only_api is enabled" do
+ test "debug_exception_response_format is :api by default if api_only is enabled" do
add_to_config <<-RUBY
config.api_only = true
RUBY
@@ -1453,7 +1464,7 @@ module ApplicationTests
assert_equal :api, Rails.configuration.debug_exception_response_format
end
- test "debug_exception_response_format can be override" do
+ test "debug_exception_response_format can be overriden" do
add_to_config <<-RUBY
config.api_only = true
RUBY
diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb
index 7bf123d12b..ea68e63f8f 100644
--- a/railties/test/application/console_test.rb
+++ b/railties/test/application/console_test.rb
@@ -52,12 +52,11 @@ class ConsoleTest < ActiveSupport::TestCase
a = b = c = nil
# TODO: These should be defined on the initializer
- ActionDispatch::Reloader.to_cleanup { a = b = c = 1 }
- ActionDispatch::Reloader.to_cleanup { b = c = 2 }
- ActionDispatch::Reloader.to_prepare { c = 3 }
+ ActiveSupport::Reloader.to_complete { a = b = c = 1 }
+ ActiveSupport::Reloader.to_complete { b = c = 2 }
+ ActiveSupport::Reloader.to_prepare { c = 3 }
- # Hide Reloading... output
- silence_stream(STDOUT) { irb_context.reload! }
+ irb_context.reload!(false)
assert_equal 1, a
assert_equal 2, b
@@ -81,7 +80,7 @@ class ConsoleTest < ActiveSupport::TestCase
MODEL
assert !User.new.respond_to?(:age)
- silence_stream(STDOUT) { irb_context.reload! }
+ irb_context.reload!(false)
assert User.new.respond_to?(:age)
end
diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb
index 84cc6e120b..644af0e737 100644
--- a/railties/test/application/generators_test.rb
+++ b/railties/test/application/generators_test.rb
@@ -160,5 +160,15 @@ module ApplicationTests
assert Rails::Generators.options[:rails][:helper]
assert_equal :my_template, Rails::Generators.options[:rails][:template_engine]
end
+
+ test "api only generator generate mailer views" do
+ add_to_config <<-RUBY
+ config.api_only = true
+ RUBY
+
+ FileUtils.cd(rails_root){ `bin/rails generate mailer notifier foo` }
+ assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.text.erb"))
+ assert File.exist?(File.join(rails_root, "app/views/notifier_mailer/foo.html.erb"))
+ end
end
end
diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb
index 4c06b6324c..44209a52f7 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -16,7 +16,7 @@ module ApplicationTests
# AC & AM
test "set load paths set only if action controller or action mailer are in use" do
- assert_nothing_raised NameError do
+ assert_nothing_raised do
add_to_config <<-RUBY
config.root = "#{app_path}"
RUBY
diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb
index ab7f29b0f2..0f9bb41053 100644
--- a/railties/test/application/initializers/i18n_test.rb
+++ b/railties/test/application/initializers/i18n_test.rb
@@ -245,7 +245,7 @@ fr:
assert_fallbacks de: [:de, :'en-US', :en]
end
- test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping de-AT => de-DE" do
+ test "[shortcut] config.i18n.fallbacks = [{ :ca => :'es-ES' }] initializes fallbacks with a mapping ca => es-ES" do
I18n::Railtie.config.i18n.fallbacks.map = { :ca => :'es-ES' }
load_app
assert_fallbacks ca: [:ca, :"es-ES", :es, :en]
@@ -257,6 +257,12 @@ fr:
assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en]
end
+ test "[shortcut] config.i18n.fallbacks = { ca: :en } initializes fallbacks with a mapping ca => :en" do
+ I18n::Railtie.config.i18n.fallbacks = { ca: :en }
+ load_app
+ assert_fallbacks ca: [:ca, :en]
+ end
+
test "disable config.i18n.enforce_available_locales" do
add_to_config <<-RUBY
config.i18n.enforce_available_locales = false
diff --git a/railties/test/application/integration_test_case_test.rb b/railties/test/application/integration_test_case_test.rb
new file mode 100644
index 0000000000..d106d5159a
--- /dev/null
+++ b/railties/test/application/integration_test_case_test.rb
@@ -0,0 +1,46 @@
+require 'isolation/abstract_unit'
+
+module ApplicationTests
+ class IntegrationTestCaseTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ setup do
+ build_app
+ boot_rails
+ end
+
+ teardown do
+ teardown_app
+ end
+
+ test "resets Action Mailer test deliveries" do
+ script('generate mailer BaseMailer welcome')
+
+ app_file 'test/integration/mailer_integration_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class MailerIntegrationTest < ActionDispatch::IntegrationTest
+ setup do
+ @old_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+ end
+
+ teardown do
+ ActionMailer::Base.delivery_method = @old_delivery_method
+ end
+
+ 2.times do |i|
+ define_method "test_resets_deliveries_\#{i}" do
+ BaseMailer.welcome.deliver_now
+ assert_equal 1, ActionMailer::Base.deliveries.count
+ end
+ end
+ end
+ RUBY
+
+ output = Dir.chdir(app_path) { `bin/rails test 2>&1` }
+ assert_equal 0, $?.to_i, output
+ assert_match(/0 failures, 0 errors/, output)
+ end
+ end
+end
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index 40abaf860d..efb21ae473 100644
--- a/railties/test/application/loading_test.rb
+++ b/railties/test/application/loading_test.rb
@@ -56,10 +56,10 @@ class LoadingTest < ActiveSupport::TestCase
require "#{rails_root}/config/environment"
- assert_nothing_raised(NameError) { Trackable }
- assert_nothing_raised(NameError) { EmailLoggable }
- assert_nothing_raised(NameError) { Orderable }
- assert_nothing_raised(NameError) { Matchable }
+ assert_nothing_raised { Trackable }
+ assert_nothing_raised { EmailLoggable }
+ assert_nothing_raised { Orderable }
+ assert_nothing_raised { Matchable }
end
test "models without table do not panic on scope definitions when loaded" do
diff --git a/railties/test/application/middleware/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb
index 7b4babb13b..639b01b562 100644
--- a/railties/test/application/middleware/exceptions_test.rb
+++ b/railties/test/application/middleware/exceptions_test.rb
@@ -85,7 +85,7 @@ module ApplicationTests
test "unspecified route when action_dispatch.show_exceptions is set shows 404" do
app.config.action_dispatch.show_exceptions = true
- assert_nothing_raised(ActionController::RoutingError) do
+ assert_nothing_raised do
get '/foo'
assert_match "The page you were looking for doesn't exist.", last_response.body
end
@@ -95,7 +95,7 @@ module ApplicationTests
app.config.action_dispatch.show_exceptions = true
app.config.consider_all_requests_local = true
- assert_nothing_raised(ActionController::RoutingError) do
+ assert_nothing_raised do
get '/foo'
assert_match "No route matches", last_response.body
end
diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb
index 97d5b5c698..37bd8a25c1 100644
--- a/railties/test/application/middleware/remote_ip_test.rb
+++ b/railties/test/application/middleware/remote_ip_test.rb
@@ -36,10 +36,10 @@ module ApplicationTests
test "works with both headers individually" do
make_basic_app
- assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_nothing_raised do
assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1")
end
- assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_nothing_raised do
assert_equal "1.1.1.2", remote_ip("HTTP_CLIENT_IP" => "1.1.1.2")
end
end
@@ -49,7 +49,7 @@ module ApplicationTests
app.config.action_dispatch.ip_spoofing_check = false
end
- assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_nothing_raised do
assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2")
end
end
diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index f847e80471..85e7761727 100644
--- a/railties/test/application/middleware/session_test.rb
+++ b/railties/test/application/middleware/session_test.rb
@@ -345,5 +345,33 @@ module ApplicationTests
get '/foo/read_raw_cookie'
assert_equal 2, verifier.verify(last_response.body)['foo']
end
+
+ test 'calling reset_session on request does not trigger an error for API apps' do
+ add_to_config 'config.api_only = true'
+
+ controller :test, <<-RUBY
+ class TestController < ApplicationController
+ def dump_flash
+ request.reset_session
+ render plain: 'It worked!'
+ end
+ end
+ RUBY
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get '/dump_flash' => "test#dump_flash"
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ get '/dump_flash'
+
+ assert_equal 200, last_response.status
+ assert_equal 'It worked!', last_response.body
+
+ refute Rails.application.middleware.include?(ActionDispatch::Flash)
+ end
end
end
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index 1434522cce..5869ff64bc 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -26,7 +26,7 @@ module ApplicationTests
assert_equal [
"Rack::Sendfile",
"ActionDispatch::Static",
- "ActionDispatch::LoadInterlock",
+ "ActionDispatch::Executor",
"ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"Rack::MethodOverride",
@@ -38,8 +38,6 @@ module ApplicationTests
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement",
- "ActiveRecord::QueryCache",
"ActionDispatch::Cookies",
"ActionDispatch::Session::CookieStore",
"ActionDispatch::Flash",
@@ -57,7 +55,7 @@ module ApplicationTests
assert_equal [
"Rack::Sendfile",
"ActionDispatch::Static",
- "ActionDispatch::LoadInterlock",
+ "ActionDispatch::Executor",
"ActiveSupport::Cache::Strategy::LocalCache",
"Rack::Runtime",
"ActionDispatch::RequestId",
@@ -67,8 +65,6 @@ module ApplicationTests
"ActionDispatch::RemoteIp",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
- "ActiveRecord::ConnectionAdapters::ConnectionManagement",
- "ActiveRecord::QueryCache",
"Rack::Head",
"Rack::ConditionalGet",
"Rack::ETag"
@@ -114,23 +110,12 @@ module ApplicationTests
test "removing Active Record omits its middleware" do
use_frameworks []
boot!
- assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement")
- assert !middleware.include?("ActiveRecord::QueryCache")
assert !middleware.include?("ActiveRecord::Migration::CheckPending")
end
- test "includes interlock if cache_classes is set but eager_load is not" do
- add_to_config "config.cache_classes = true"
- boot!
- assert_not_includes middleware, "Rack::Lock"
- assert_includes middleware, "ActionDispatch::LoadInterlock"
- end
-
- test "includes interlock if cache_classes is off" do
- add_to_config "config.cache_classes = false"
+ test "includes executor" do
boot!
- assert_not_includes middleware, "Rack::Lock"
- assert_includes middleware, "ActionDispatch::LoadInterlock"
+ assert_includes middleware, "ActionDispatch::Executor"
end
test "does not include lock if cache_classes is set and so is eager_load" do
@@ -138,21 +123,18 @@ module ApplicationTests
add_to_config "config.eager_load = true"
boot!
assert_not_includes middleware, "Rack::Lock"
- assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
test "does not include lock if allow_concurrency is set to :unsafe" do
add_to_config "config.allow_concurrency = :unsafe"
boot!
assert_not_includes middleware, "Rack::Lock"
- assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
test "includes lock if allow_concurrency is disabled" do
add_to_config "config.allow_concurrency = false"
boot!
assert_includes middleware, "Rack::Lock"
- assert_not_includes middleware, "ActionDispatch::LoadInterlock"
end
test "removes static asset server if public_file_server.enabled is disabled" do
diff --git a/railties/test/application/per_request_digest_cache_test.rb b/railties/test/application/per_request_digest_cache_test.rb
index 3198e12662..dfe3fc9354 100644
--- a/railties/test/application/per_request_digest_cache_test.rb
+++ b/railties/test/application/per_request_digest_cache_test.rb
@@ -29,6 +29,8 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase
app_file 'app/controllers/customers_controller.rb', <<-RUBY
class CustomersController < ApplicationController
+ self.perform_caching = true
+
def index
render [ Customer.new('david', 1), Customer.new('dingus', 2) ]
end
@@ -50,12 +52,13 @@ class PerRequestDigestCacheTest < ActiveSupport::TestCase
get '/customers'
assert_equal 200, last_response.status
- assert_equal [ '8ba099b7749542fe765ff34a6824d548' ], ActionView::Digestor.cache.values
+ values = ActionView::LookupContext::DetailsKey.digest_caches.first.values
+ assert_equal [ '8ba099b7749542fe765ff34a6824d548' ], values
assert_equal %w(david dingus), last_response.body.split.map(&:strip)
end
test "template digests are cleared before a request" do
- assert_called(ActionView::Digestor.cache, :clear) do
+ assert_called(ActionView::LookupContext::DetailsKey, :clear) do
get '/customers'
assert_equal 200, last_response.status
end
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index a229609e84..cee9db5535 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -29,11 +29,11 @@ module ApplicationTests
def db_create_and_drop(expected_database)
Dir.chdir(app_path) do
output = `bin/rails db:create`
- assert_empty output
+ assert_match(/Created database/, output)
assert File.exist?(expected_database)
assert_equal expected_database, ActiveRecord::Base.connection_config[:database]
output = `bin/rails db:drop`
- assert_empty output
+ assert_match(/Dropped database/, output)
assert !File.exist?(expected_database)
end
end
diff --git a/railties/test/application/rake/dev_test.rb b/railties/test/application/rake/dev_test.rb
index 43d7a5e156..deb9bc8dee 100644
--- a/railties/test/application/rake/dev_test.rb
+++ b/railties/test/application/rake/dev_test.rb
@@ -15,7 +15,7 @@ module ApplicationTests
test 'dev:cache creates file and outputs message' do
Dir.chdir(app_path) do
- output = `rake dev:cache`
+ output = `rails dev:cache`
assert File.exist?('tmp/caching-dev.txt')
assert_match(/Development mode is now being cached/, output)
end
@@ -29,6 +29,15 @@ module ApplicationTests
assert_match(/Development mode is no longer being cached/, output)
end
end
+
+ test 'dev:cache removes server.pid also' do
+ Dir.chdir(app_path) do
+ FileUtils.mkdir_p("tmp/pids")
+ FileUtils.touch("tmp/pids/server.pid")
+ `rake dev:cache`
+ assert_not File.exist?("tmp/pids/server.pid")
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake/restart_test.rb b/railties/test/application/rake/restart_test.rb
index 4cae199e6b..30f662a9be 100644
--- a/railties/test/application/rake/restart_test.rb
+++ b/railties/test/application/rake/restart_test.rb
@@ -34,6 +34,15 @@ module ApplicationTests
assert File.exist?('tmp/restart.txt')
end
end
+
+ test 'rake restart removes server.pid also' do
+ Dir.chdir(app_path) do
+ FileUtils.mkdir_p("tmp/pids")
+ FileUtils.touch("tmp/pids/server.pid")
+ `rake restart`
+ assert_not File.exist?("tmp/pids/server.pid")
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index 3d3e47de8d..1a786a3fd3 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -24,7 +24,7 @@ module ApplicationTests
assert $task_loaded
end
- def test_the_test_rake_task_is_protected_when_previous_migration_was_production
+ test "task is protected when previous migration was production" do
Dir.chdir(app_path) do
output = `bin/rails generate model product name:string;
env RAILS_ENV=production bin/rails db:create db:migrate;
@@ -118,11 +118,11 @@ module ApplicationTests
end
def test_code_statistics_sanity
- assert_match "Code LOC: 14 Test LOC: 0 Code to Test Ratio: 1:0.0",
+ assert_match "Code LOC: 18 Test LOC: 0 Code to Test Ratio: 1:0.0",
Dir.chdir(app_path){ `bin/rails stats` }
end
- def test_rake_routes_calls_the_route_inspector
+ def test_rails_routes_calls_the_route_inspector
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/cart', to: 'cart#show'
@@ -133,7 +133,7 @@ module ApplicationTests
assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
end
- def test_rake_routes_with_controller_environment
+ def test_rails_routes_with_controller_environment
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/cart', to: 'cart#show'
@@ -151,7 +151,7 @@ module ApplicationTests
assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
end
- def test_rake_routes_with_namespaced_controller_environment
+ def test_rails_routes_with_namespaced_controller_environment
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
namespace :admin do
@@ -175,19 +175,27 @@ module ApplicationTests
assert_equal expected_output, output
end
- def test_rake_routes_with_global_search_key
+ def test_rails_routes_with_global_search_key
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/cart', to: 'cart#show'
- get '/basketball', to: 'basketball#index'
+ post '/cart', to: 'cart#create'
+ get '/basketballs', to: 'basketball#index'
end
RUBY
output = Dir.chdir(app_path){ `bin/rails routes -g show` }
assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
+
+ output = Dir.chdir(app_path){ `bin/rails routes -g POST` }
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n POST /cart(.:format) cart#create\n", output
+
+ output = Dir.chdir(app_path){ `bin/rails routes -g basketballs` }
+ assert_equal " Prefix Verb URI Pattern Controller#Action\n" \
+ "basketballs GET /basketballs(.:format) basketball#index\n", output
end
- def test_rake_routes_with_controller_search_key
+ def test_rails_routes_with_controller_search_key
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
get '/cart', to: 'cart#show'
@@ -205,7 +213,7 @@ module ApplicationTests
assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
end
- def test_rake_routes_displays_message_when_no_routes_are_defined
+ def test_rails_routes_displays_message_when_no_routes_are_defined
app_file "config/routes.rb", <<-RUBY
Rails.application.routes.draw do
end
@@ -361,7 +369,7 @@ module ApplicationTests
def test_copy_templates
Dir.chdir(app_path) do
- `bin/rails rails:templates:copy`
+ `bin/rails app:templates:copy`
%w(controller mailer scaffold).each do |dir|
assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir))
end
@@ -376,7 +384,7 @@ module ApplicationTests
app_file "template.rb", ""
output = Dir.chdir(app_path) do
- `bin/rails rails:template LOCATION=template.rb`
+ `bin/rails app:template LOCATION=template.rb`
end
assert_match(/Hello, World!/, output)
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
index 7ecadb60ca..a1735db5b3 100644
--- a/railties/test/application/test_runner_test.rb
+++ b/railties/test/application/test_runner_test.rb
@@ -467,7 +467,7 @@ module ApplicationTests
create_test_file :models, 'post', pass: false
output = run_test_command('test/models/post_test.rb')
- expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth:\nwups!\n\nbin/rails test test/models/post_test.rb:6\n\n\n\n}
+ expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/models/post_test.rb:6\]:\nwups!\n\nbin/rails test test/models/post_test.rb:4\n\n\n\n}
assert_match expect, output
end
@@ -502,6 +502,32 @@ module ApplicationTests
assert_match '1 runs, 1 assertions', output
end
+ def test_rails_db_create_all_restores_db_connection
+ create_test_file :models, 'account'
+ output = Dir.chdir(app_path) { `bin/rails db:create:all db:migrate && echo ".tables" | rails dbconsole` }
+ assert_match "ar_internal_metadata", output, "tables should be dumped"
+ end
+
+ def test_rails_db_create_all_restores_db_connection_after_drop
+ create_test_file :models, 'account'
+ Dir.chdir(app_path) { `bin/rails db:create:all` } # create all to avoid warnings
+ output = Dir.chdir(app_path) { `bin/rails db:drop:all db:create:all db:migrate && echo ".tables" | rails dbconsole` }
+ assert_match "ar_internal_metadata", output, "tables should be dumped"
+ end
+
+ def test_rake_passes_TESTOPTS_to_minitest
+ create_test_file :models, 'account'
+ output = Dir.chdir(app_path) { `bin/rake test TESTOPTS=-v` }
+ assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test"
+ end
+
+ def test_rake_passes_multiple_TESTOPTS_to_minitest
+ create_test_file :models, 'account'
+ output = Dir.chdir(app_path) { `bin/rake test TESTOPTS='-v --seed=1234'` }
+ assert_match "AccountTest#test_truth", output, "passing TEST= should run selected test"
+ assert_match "seed=1234", output, "passing TEST= should run selected test"
+ end
+
private
def run_test_command(arguments = 'test/unit/test_test.rb')
Dir.chdir(app_path) { `bin/rails t #{arguments}` }
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index 0c49bd9c53..38a1605d1f 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -54,7 +54,8 @@ class Rails::ServerTest < ActiveSupport::TestCase
def test_caching_without_option
args = []
options = Rails::Server::Options.new.parse!(args)
- assert_equal nil, options[:caching]
+ merged_options = Rails::Server.new.default_options.merge(options)
+ assert_equal nil, merged_options[:caching]
end
def test_caching_with_option
@@ -117,4 +118,18 @@ class Rails::ServerTest < ActiveSupport::TestCase
assert_equal old_default_options, server.default_options
end
end
+
+ def test_restart_command_contains_customized_options
+ original_args = ARGV.dup
+ args = ["-p", "4567"]
+ ARGV.replace args
+
+ options = Rails::Server::Options.new.parse! args
+ server = Rails::Server.new options
+ expected = "bin/rails server -p 4567"
+
+ assert_equal expected, server.default_options[:restart_cmd]
+ ensure
+ ARGV.replace original_args
+ end
end
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 3300850604..3b2b3c37d0 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -201,7 +201,7 @@ class ActionsTest < Rails::Generators::TestCase
end
end
- def test_rake_should_run_rake_command_with_default_env
+ def test_rails_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
action :rake, 'log:clear'
@@ -209,13 +209,13 @@ class ActionsTest < Rails::Generators::TestCase
end
end
- def test_rake_with_env_option_should_run_rake_command_in_env
+ def test_rails_with_env_option_should_run_rake_command_in_env
assert_called_with(generator, :run, ['rake log:clear RAILS_ENV=production', verbose: false]) do
action :rake, 'log:clear', env: 'production'
end
end
- def test_rake_with_rails_env_variable_should_run_rake_command_in_env
+ test "rails command with RAILS_ENV variable should run rake command in env" do
assert_called_with(generator, :run, ['rake log:clear RAILS_ENV=production', verbose: false]) do
with_rails_env "production" do
action :rake, 'log:clear'
@@ -223,7 +223,7 @@ class ActionsTest < Rails::Generators::TestCase
end
end
- def test_env_option_should_win_over_rails_env_variable_when_running_rake
+ test "env option should win over RAILS_ENV variable when running rake" do
assert_called_with(generator, :run, ['rake log:clear RAILS_ENV=production', verbose: false]) do
with_rails_env "staging" do
action :rake, 'log:clear', env: 'production'
@@ -231,7 +231,7 @@ class ActionsTest < Rails::Generators::TestCase
end
end
- def test_rake_with_sudo_option_should_run_rake_command_with_sudo
+ test "rails command with sudo option should run rake command with sudo" do
assert_called_with(generator, :run, ["sudo rake log:clear RAILS_ENV=development", verbose: false]) do
with_rails_env nil do
action :rake, 'log:clear', sudo: true
@@ -239,6 +239,44 @@ class ActionsTest < Rails::Generators::TestCase
end
end
+ test "rails command should run rails_command with default env" do
+ assert_called_with(generator, :run, ["rails log:clear RAILS_ENV=development", verbose: false]) do
+ with_rails_env nil do
+ action :rails_command, 'log:clear'
+ end
+ end
+ end
+
+ test "rails command with env option should run rails_command with same env" do
+ assert_called_with(generator, :run, ['rails log:clear RAILS_ENV=production', verbose: false]) do
+ action :rails_command, 'log:clear', env: 'production'
+ end
+ end
+
+ test "rails command with RAILS_ENV variable should run rails_command in env" do
+ assert_called_with(generator, :run, ['rails log:clear RAILS_ENV=production', verbose: false]) do
+ with_rails_env "production" do
+ action :rails_command, 'log:clear'
+ end
+ end
+ end
+
+ def test_env_option_should_win_over_rails_env_variable_when_running_rails
+ assert_called_with(generator, :run, ['rails log:clear RAILS_ENV=production', verbose: false]) do
+ with_rails_env "staging" do
+ action :rails_command, 'log:clear', env: 'production'
+ end
+ end
+ end
+
+ test "rails command with sudo option should run rails_command with sudo" do
+ assert_called_with(generator, :run, ["sudo rails log:clear RAILS_ENV=development", verbose: false]) do
+ with_rails_env nil do
+ action :rails_command, 'log:clear', sudo: true
+ end
+ end
+ end
+
def test_capify_should_run_the_capify_command
assert_called_with(generator, :run, ['capify .', verbose: false]) do
action :capify!
diff --git a/railties/test/generators/api_app_generator_test.rb b/railties/test/generators/api_app_generator_test.rb
index 1ea5661006..8e1cd0891a 100644
--- a/railties/test/generators/api_app_generator_test.rb
+++ b/railties/test/generators/api_app_generator_test.rb
@@ -73,6 +73,8 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
app/controllers
app/mailers
app/models
+ app/views/layouts/mailer.html.erb
+ app/views/layouts/mailer.text.erb
config/environments
config/initializers
config/locales
@@ -94,7 +96,7 @@ class ApiAppGeneratorTest < Rails::Generators::TestCase
def skipped_files
%w(app/assets
app/helpers
- app/views
+ app/views/layouts/application.html.erb
config/initializers/assets.rb
config/initializers/cookies_serializer.rb
config/initializers/session_store.rb
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index be05e779ea..2d9867fa9d 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -28,6 +28,7 @@ DEFAULT_APP_FILES = %w(
config/locales
config/cable.yml
config/puma.rb
+ config/spring.rb
db
lib
lib/tasks
@@ -64,8 +65,8 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_assets
run_generator
- assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track' => true/)
- assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track' => true/)
+ assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track': 'reload'/)
+ assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track': 'reload'/)
assert_file("app/assets/stylesheets/application.css")
assert_file("app/assets/javascripts/application.js")
end
@@ -125,7 +126,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
# make sure we are in correct dir
FileUtils.cd(app_moved_root)
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
+ generator = Rails::Generators::AppGenerator.new ["rails"], [],
destination_root: app_moved_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
@@ -140,7 +141,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [app_root]
stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/
@@ -164,7 +165,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [app_root]
stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
@@ -178,7 +179,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.rm("#{app_root}/config/initializers/callback_terminator.rb")
stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
assert_no_file "#{app_root}/config/initializers/callback_terminator.rb"
@@ -192,24 +193,39 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.touch("#{app_root}/config/initializers/callback_terminator.rb")
stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
assert_file "#{app_root}/config/initializers/callback_terminator.rb"
end
end
- def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured
+ def test_rails_update_set_the_cookie_serializer_to_marshal_if_it_is_not_already_configured
app_root = File.join(destination_root, 'myapp')
run_generator [app_root]
FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb")
stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
- assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb",
+ /Valid options are :json, :marshal, and :hybrid\.\nRails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
+ end
+ end
+
+ def test_rails_update_dont_set_file_watcher
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "#{app_root}/config/environments/development.rb" do |content|
+ assert_match(/# config.file_watcher/, content)
+ end
end
end
@@ -220,7 +236,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.rm("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb")
stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
assert_no_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb"
@@ -234,13 +250,67 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.touch("#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb")
stub_rails_application(app_root) do
- generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:update_config_files) }
assert_file "#{app_root}/config/initializers/active_record_belongs_to_required_by_default.rb"
end
end
+ def test_rails_update_does_not_create_ssl_options_by_default
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ FileUtils.rm("#{app_root}/config/initializers/ssl_options.rb")
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_no_file "#{app_root}/config/initializers/ssl_options.rb"
+ end
+ end
+
+ def test_rails_update_does_not_remove_ssl_options_if_already_present
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ FileUtils.touch("#{app_root}/config/initializers/ssl_options.rb")
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "#{app_root}/config/initializers/ssl_options.rb"
+ end
+ end
+
+ def test_rails_update_does_not_create_rack_cors
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_no_file "#{app_root}/config/initializers/cors.rb"
+ end
+ end
+
+ def test_rails_update_does_not_remove_rack_cors_if_already_present
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ FileUtils.touch("#{app_root}/config/initializers/cors.rb")
+
+ stub_rails_application(app_root) do
+ generator = Rails::Generators::AppGenerator.new ["rails"], [], destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file "#{app_root}/config/initializers/cors.rb"
+ end
+ end
+
def test_application_names_are_not_singularized
run_generator [File.join(destination_root, "hats")]
assert_file "hats/config/environment.rb", /Rails\.application\.initialize!/
@@ -338,6 +408,11 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_generator_defaults_to_puma_version
+ run_generator [destination_root]
+ assert_gem "puma", "'~> 3.0'"
+ end
+
def test_generator_if_skip_puma_is_given
run_generator [destination_root, "--skip-puma"]
assert_no_file "config/puma.rb"
@@ -404,11 +479,8 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "--skip-action-cable"]
assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/
assert_no_file "config/cable.yml"
- assert_no_file "app/assets/javascripts/cable.coffee"
+ assert_no_file "app/assets/javascripts/cable.js"
assert_no_file "app/channels"
- assert_file "app/views/layouts/application.html.erb" do |content|
- assert_no_match(/action_cable_meta_tag/, content)
- end
assert_file "Gemfile" do |content|
assert_no_match(/redis/, content)
end
@@ -479,18 +551,20 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_inclusion_of_listen_related_gems
+ def test_inclusion_of_listen_related_configuration_by_default
run_generator
if RbConfig::CONFIG['host_os'] =~ /darwin|linux/
- assert_gem 'listen'
- assert_gem 'spring-watcher-listen'
+ assert_listen_related_configuration
else
- assert_file 'Gemfile' do |content|
- assert_no_match(/listen/, content)
- end
+ assert_no_listen_related_configuration
end
end
+ def test_non_inclusion_of_listen_related_configuration_if_skip_listen
+ run_generator [destination_root, '--skip-listen']
+ assert_no_listen_related_configuration
+ end
+
def test_evented_file_update_checker_config
run_generator
assert_file 'config/environments/development.rb' do |content|
@@ -572,7 +646,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |content|
assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content)
- assert_no_match(/gem 'web-console', '~> 3.0'/, content)
+ assert_no_match(/\Agem 'web-console'\z/, content)
end
end
@@ -581,7 +655,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile" do |content|
assert_match(/gem 'web-console',\s+github: 'rails\/web-console'/, content)
- assert_no_match(/gem 'web-console', '~> 3.0'/, content)
+ assert_no_match(/\Agem 'web-console'\z/, content)
end
end
@@ -611,7 +685,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_spring_no_fork
jruby_skip "spring doesn't run on JRuby"
- assert_called_with(Process, :respond_to?, [:fork], returns: false) do
+ assert_called_with(Process, :respond_to?, [[:fork], [:fork]], returns: false) do
run_generator
assert_file "Gemfile" do |content|
@@ -623,6 +697,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_skip_spring
run_generator [destination_root, "--skip-spring"]
+ assert_no_file 'config/spring.rb'
assert_file "Gemfile" do |content|
assert_no_match(/spring/, content)
end
@@ -759,4 +834,23 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile", /^\s*gem\s+["']#{gem}["']$*/
end
end
+
+ def assert_listen_related_configuration
+ assert_gem 'listen'
+ assert_gem 'spring-watcher-listen'
+
+ assert_file 'config/environments/development.rb' do |content|
+ assert_match(/^\s*config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
+ end
+ end
+
+ def assert_no_listen_related_configuration
+ assert_file 'Gemfile' do |content|
+ assert_no_match(/listen/, content)
+ end
+
+ assert_file 'config/environments/development.rb' do |content|
+ assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, content)
+ end
+ end
end
diff --git a/railties/test/generators/channel_generator_test.rb b/railties/test/generators/channel_generator_test.rb
index c1f0c03fbf..d58b54ac24 100644
--- a/railties/test/generators/channel_generator_test.rb
+++ b/railties/test/generators/channel_generator_test.rb
@@ -5,6 +5,18 @@ class ChannelGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
tests Rails::Generators::ChannelGenerator
+ def test_application_cable_skeleton_is_created
+ run_generator ['books']
+
+ assert_file "app/channels/application_cable/channel.rb" do |cable|
+ assert_match(/module ApplicationCable\n class Channel < ActionCable::Channel::Base\n/, cable)
+ end
+
+ assert_file "app/channels/application_cable/connection.rb" do |cable|
+ assert_match(/module ApplicationCable\n class Connection < ActionCable::Connection::Base\n/, cable)
+ end
+ end
+
def test_channel_is_created
run_generator ['chat']
@@ -26,4 +38,24 @@ class ChannelGeneratorTest < Rails::Generators::TestCase
assert_no_file "app/assets/javascripts/channels/chat.coffee"
end
+
+ def test_cable_js_is_created_if_not_present_already
+ run_generator ['chat']
+ FileUtils.rm("#{destination_root}/app/assets/javascripts/cable.js")
+ run_generator ['camp']
+
+ assert_file "app/assets/javascripts/cable.js"
+ end
+
+ def test_channel_on_revoke
+ run_generator ['chat']
+ run_generator ['chat'], behavior: :revoke
+
+ assert_no_file "app/channels/chat_channel.rb"
+ assert_no_file "app/assets/javascripts/channels/chat.coffee"
+
+ assert_file "app/channels/application_cable/channel.rb"
+ assert_file "app/channels/application_cable/connection.rb"
+ assert_file "app/assets/javascripts/cable.js"
+ end
end
diff --git a/railties/test/generators/job_generator_test.rb b/railties/test/generators/job_generator_test.rb
index 7fd8f2062f..dbff0ab704 100644
--- a/railties/test/generators/job_generator_test.rb
+++ b/railties/test/generators/job_generator_test.rb
@@ -26,4 +26,11 @@ class JobGeneratorTest < Rails::Generators::TestCase
assert_match(/queue_as :admin/, job)
end
end
+
+ def test_application_job_skeleton_is_created
+ run_generator ["refresh_counters"]
+ assert_file "app/jobs/application_job.rb" do |job|
+ assert_match(/class ApplicationJob < ActiveJob::Base/, job)
+ end
+ end
end
diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb
index f8d9ccacb4..8728b39dae 100644
--- a/railties/test/generators/mailer_generator_test.rb
+++ b/railties/test/generators/mailer_generator_test.rb
@@ -12,6 +12,12 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_no_match(/default from: "from@example.com"/, mailer)
assert_no_match(/layout :mailer_notifier/, mailer)
end
+
+ assert_file 'app/mailers/application_mailer.rb' do |mailer|
+ assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer)
+ assert_match(/default from: 'from@example.com'/, mailer)
+ assert_match(/layout 'mailer'/, mailer)
+ end
end
def test_mailer_with_i18n_helper
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index c8c8f0aa3b..ed6846abc3 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -6,6 +6,14 @@ class ModelGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
arguments %w(Account name:string age:integer)
+ def test_application_record_skeleton_is_created
+ run_generator
+ assert_file "app/models/application_record.rb" do |record|
+ assert_match(/class ApplicationRecord < ActiveRecord::Base/, record)
+ assert_match(/self.abstract_class = true/, record)
+ end
+ end
+
def test_help_shows_invoked_generators_options
content = run_generator ["--help"]
assert_match(/ActiveRecord options:/, content)
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
index 6e5132e849..3cc8e1de55 100644
--- a/railties/test/generators/plugin_generator_test.rb
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -335,7 +335,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "hyphenated-name/lib/hyphenated/name/engine.rb", /module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Hyphenated::Name\n end\n end\nend/
assert_file "hyphenated-name/lib/hyphenated/name.rb", /require "hyphenated\/name\/engine"/
assert_file "hyphenated-name/test/dummy/config/routes.rb", /mount Hyphenated::Name::Engine => "\/hyphenated-name"/
- assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery :with => :exception\n end\n end\nend\n/
+ assert_file "hyphenated-name/app/controllers/hyphenated/name/application_controller.rb", /module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/
assert_file "hyphenated-name/app/models/hyphenated/name/application_record.rb", /module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/
assert_file "hyphenated-name/app/jobs/hyphenated/name/application_job.rb", /module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/
assert_file "hyphenated-name/app/mailers/hyphenated/name/application_mailer.rb", /module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/
@@ -357,7 +357,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "my_hyphenated-name/lib/my_hyphenated/name/engine.rb", /module MyHyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace MyHyphenated::Name\n end\n end\nend/
assert_file "my_hyphenated-name/lib/my_hyphenated/name.rb", /require "my_hyphenated\/name\/engine"/
assert_file "my_hyphenated-name/test/dummy/config/routes.rb", /mount MyHyphenated::Name::Engine => "\/my_hyphenated-name"/
- assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery :with => :exception\n end\n end\nend\n/
+ assert_file "my_hyphenated-name/app/controllers/my_hyphenated/name/application_controller.rb", /module MyHyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\nend\n/
assert_file "my_hyphenated-name/app/models/my_hyphenated/name/application_record.rb", /module MyHyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\nend/
assert_file "my_hyphenated-name/app/jobs/my_hyphenated/name/application_job.rb", /module MyHyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/
assert_file "my_hyphenated-name/app/mailers/my_hyphenated/name/application_mailer.rb", /module MyHyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\nend/
@@ -379,7 +379,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "deep-hyphenated-name/lib/deep/hyphenated/name/engine.rb", /module Deep\n module Hyphenated\n module Name\n class Engine < ::Rails::Engine\n isolate_namespace Deep::Hyphenated::Name\n end\n end\n end\nend/
assert_file "deep-hyphenated-name/lib/deep/hyphenated/name.rb", /require "deep\/hyphenated\/name\/engine"/
assert_file "deep-hyphenated-name/test/dummy/config/routes.rb", /mount Deep::Hyphenated::Name::Engine => "\/deep-hyphenated-name"/
- assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery :with => :exception\n end\n end\n end\nend\n/
+ assert_file "deep-hyphenated-name/app/controllers/deep/hyphenated/name/application_controller.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationController < ActionController::Base\n protect_from_forgery with: :exception\n end\n end\n end\nend\n/
assert_file "deep-hyphenated-name/app/models/deep/hyphenated/name/application_record.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationRecord < ActiveRecord::Base\n self\.abstract_class = true\n end\n end\n end\nend/
assert_file "deep-hyphenated-name/app/jobs/deep/hyphenated/name/application_job.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationJob < ActiveJob::Base/
assert_file "deep-hyphenated-name/app/mailers/deep/hyphenated/name/application_mailer.rb", /module Deep\n module Hyphenated\n module Name\n class ApplicationMailer < ActionMailer::Base\n default from: 'from@example.com'\n layout 'mailer'\n end\n end\n end\nend/
@@ -444,6 +444,14 @@ class PluginGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_dummy_appplication_skip_listen_by_default
+ run_generator
+
+ assert_file 'test/dummy/config/environments/development.rb' do |contents|
+ assert_match(/^\s*# config.file_watcher = ActiveSupport::EventedFileUpdateChecker/, contents)
+ end
+ end
+
def test_ensure_that_gitignore_can_be_generated_from_a_template_for_dummy_path
FileUtils.cd(Rails.root)
run_generator([destination_root, "--dummy_path", "spec/dummy", "--skip-test"])
@@ -634,6 +642,46 @@ class PluginGeneratorTest < Rails::Generators::TestCase
assert_file "app/models/bukkits/article.rb", /class Article < ApplicationRecord/
end
+ def test_generate_application_record_when_does_not_exist_in_mountable_engine
+ run_generator [destination_root, '--mountable']
+ FileUtils.rm "#{destination_root}/app/models/bukkits/application_record.rb"
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g model article`
+ end
+
+ assert_file "#{destination_root}/app/models/bukkits/application_record.rb" do |record|
+ assert_match(/module Bukkits/, record)
+ assert_match(/class ApplicationRecord < ActiveRecord::Base/, record)
+ assert_match(/self.abstract_class = true/, record)
+ end
+ end
+
+ def test_generate_application_mailer_when_does_not_exist_in_mountable_engine
+ run_generator [destination_root, '--mountable']
+ FileUtils.rm "#{destination_root}/app/mailers/bukkits/application_mailer.rb"
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g mailer User`
+ end
+
+ assert_file "#{destination_root}/app/mailers/bukkits/application_mailer.rb" do |mailer|
+ assert_match(/module Bukkits/, mailer)
+ assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer)
+ end
+ end
+
+ def test_generate_application_job_when_does_not_exist_in_mountable_engine
+ run_generator [destination_root, '--mountable']
+ FileUtils.rm "#{destination_root}/app/jobs/bukkits/application_job.rb"
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g job refresh_counters`
+ end
+
+ assert_file "#{destination_root}/app/jobs/bukkits/application_job.rb" do |record|
+ assert_match(/module Bukkits/, record)
+ assert_match(/class ApplicationJob < ActiveJob::Base/, record)
+ end
+ end
+
def test_after_bundle_callback
path = 'http://example.org/rails_template'
template = %{ after_bundle { run 'echo ran after_bundle' } }
diff --git a/railties/test/generators/plugin_test_runner_test.rb b/railties/test/generators/plugin_test_runner_test.rb
index f492cd49ef..ef6359fece 100644
--- a/railties/test/generators/plugin_test_runner_test.rb
+++ b/railties/test/generators/plugin_test_runner_test.rb
@@ -63,7 +63,7 @@ class PluginTestRunnerTest < ActiveSupport::TestCase
create_test_file 'post', pass: false
output = run_test_command('test/post_test.rb')
- expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth:\nwups!\n\nbin/test (/private)?#{plugin_path}/test/post_test.rb:6}
+ expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/test (/private)?#{plugin_path}/test/post_test.rb:4}
assert_match expect, output
end
diff --git a/railties/test/generators/test_runner_in_engine_test.rb b/railties/test/generators/test_runner_in_engine_test.rb
index 69906b962b..d37e261fbb 100644
--- a/railties/test/generators/test_runner_in_engine_test.rb
+++ b/railties/test/generators/test_runner_in_engine_test.rb
@@ -17,7 +17,7 @@ class TestRunnerInEngineTest < ActiveSupport::TestCase
create_test_file 'post', pass: false
output = run_test_command('test/post_test.rb')
- expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth:\nwups!\n\nbin/rails test test/post_test.rb:6}
+ expect = %r{Running:\n\nPostTest\nF\n\nFailure:\nPostTest#test_truth \[[^\]]+test/post_test.rb:6\]:\nwups!\n\nbin/rails test test/post_test.rb:4}
assert_match expect, output
end
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index e7a261fa1f..52e0277633 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -124,7 +124,7 @@ module TestHelpers
routes = File.read("#{app_path}/config/routes.rb")
if routes =~ /(\n\s*end\s*)\Z/
File.open("#{app_path}/config/routes.rb", 'w') do |f|
- f.puts $` + "\nmatch ':controller(/:action(/:id))(.:format)', via: :all\n" + $1
+ f.puts $` + "\nActiveSupport::Deprecation.silence { match ':controller(/:action(/:id))(.:format)', via: :all }\n" + $1
end
end
@@ -154,8 +154,6 @@ module TestHelpers
config.action_controller.allow_forgery_protection = false
config.log_level = :info
RUBY
-
- remove_from_env_config('development', 'config.file_watcher.*')
end
def teardown_app
@@ -313,10 +311,6 @@ module TestHelpers
end
def boot_rails
- # FIXME: shush Sass warning spam, not relevant to testing Railties
- Kernel.silence_warnings do
- require File.expand_path('../../../../load_paths', __FILE__)
- end
end
end
end
@@ -328,7 +322,6 @@ class ActiveSupport::TestCase
include ActiveSupport::Testing::Stream
self.test_order = :sorted
-
end
# Create a scope and build a fixture rails app
@@ -339,12 +332,8 @@ Module.new do
FileUtils.rm_rf(app_template_path)
FileUtils.mkdir(app_template_path)
- environment = File.expand_path('../../../../load_paths', __FILE__)
- require_environment = "-r #{environment}"
-
- `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --no-rc`
+ `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails new #{app_template_path} --skip-gemfile --skip-listen --no-rc`
File.open("#{app_template_path}/config/boot.rb", 'w') do |f|
- f.puts "require '#{environment}'"
f.puts "require 'rails/all'"
end
end unless defined?(RAILS_ISOLATED_ENGINE)
diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb
index 5f4171d44b..b85e16c040 100644
--- a/railties/test/railties/generators_test.rb
+++ b/railties/test/railties/generators_test.rb
@@ -26,11 +26,7 @@ module RailtiesTests
end
def rails(cmd)
- environment = File.expand_path('../../../../load_paths', __FILE__)
- if File.exist?("#{environment}.rb")
- require_environment = "-r #{environment}"
- end
- `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}`
+ `#{Gem.ruby} #{RAILS_FRAMEWORK_ROOT}/railties/exe/rails #{cmd}`
end
def build_engine(is_mountable=false)
diff --git a/railties/test/test_unit/reporter_test.rb b/railties/test/test_unit/reporter_test.rb
index 2d08d4ec30..0d64b48550 100644
--- a/railties/test/test_unit/reporter_test.rb
+++ b/railties/test/test_unit/reporter_test.rb
@@ -62,7 +62,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
@reporter.record(failed_test)
@reporter.report
- expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot:\nboo\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z}
+ expect = %r{\AF\n\nFailure:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nboo\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z}
assert_match expect, @output.string
end
@@ -79,7 +79,7 @@ class TestUnitReporterTest < ActiveSupport::TestCase
verbose.record(skipped_test)
verbose.report
- expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z}
+ expect = %r{\ATestUnitReporterTest::ExampleTest#woot = 10\.00 s = S\n\n\nSkipped:\nTestUnitReporterTest::ExampleTest#woot \[[^\]]+\]:\nskipchurches, misstemples\n\nbin/rails test test/test_unit/reporter_test.rb:\d+\n\n\z}
assert_match expect, @output.string
end
@@ -104,11 +104,22 @@ class TestUnitReporterTest < ActiveSupport::TestCase
end
end
- test "fail fast does not interrupt run errors or skips" do
+ test "fail fast interrupts run on error" do
fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true
+ interrupt_raised = false
- fail_fast.record(errored_test)
- assert_no_match 'Failed tests:', @output.string
+ # Minitest passes through Interrupt, catch it manually.
+ begin
+ fail_fast.record(errored_test)
+ rescue Interrupt
+ interrupt_raised = true
+ ensure
+ assert interrupt_raised, 'Expected Interrupt to be raised.'
+ end
+ end
+
+ test "fail fast does not interrupt run skips" do
+ fail_fast = Rails::TestUnitReporter.new @output, fail_fast: true
fail_fast.record(skipped_test)
assert_no_match 'Failed tests:', @output.string
diff --git a/tools/README.md b/tools/README.md
index 25ab798bd5..b2e7e4b0ae 100644
--- a/tools/README.md
+++ b/tools/README.md
@@ -1,7 +1,8 @@
-## Rails dev tools
+# Rails dev tools
This is a collection of utilities used for Rails internal development.
They aren't used by Rails apps directly.
* `console` drops you in irb and loads local Rails repos
* `profile` profiles `Kernel#require` to help reduce startup time
+ * `line_statistics` provides CodeTools module and LineStatistics class to count lines
diff --git a/tools/console b/tools/console
index ea995a1a54..98a848ff6b 100755
--- a/tools/console
+++ b/tools/console
@@ -1,5 +1,7 @@
#!/usr/bin/env ruby
-require File.expand_path('../../load_paths', __FILE__)
+require 'bundler'
+Bundler.setup
+
require 'rails/all'
require 'active_support/all'
require 'irb'
diff --git a/tools/test.rb b/tools/test.rb
index 70f295b554..62e0faa3db 100644
--- a/tools/test.rb
+++ b/tools/test.rb
@@ -1,5 +1,8 @@
$: << File.expand_path("test", COMPONENT_ROOT)
-require File.expand_path("../../load_paths", __FILE__)
+
+require 'bundler'
+Bundler.setup
+
require "rails/test_unit/minitest_plugin"
module Rails
diff --git a/version.rb b/version.rb
index 93e0151602..081222425c 100644
--- a/version.rb
+++ b/version.rb
@@ -8,7 +8,7 @@ module Rails
MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta2"
+ PRE = "beta3"
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end